From 649eac17760abd62ccab5438620dad56c7e2eedc Mon Sep 17 00:00:00 2001 From: gunshiz Date: Sun, 16 Aug 2026 01:20:59 +0700 Subject: [PATCH] test: cover share authentication and copy behavior --- app/share/[id]/share-client.tsx | 7 +++-- lib/share/copy.ts | 5 +++ lib/share/current-author.ts | 11 +++---- lib/share/session.ts | 17 +++++++++++ lib/share/ui-contracts.test.ts | 54 +++++++++++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 lib/share/copy.ts create mode 100644 lib/share/session.ts create mode 100644 lib/share/ui-contracts.test.ts diff --git a/app/share/[id]/share-client.tsx b/app/share/[id]/share-client.tsx index 9b4da34..c883b12 100644 --- a/app/share/[id]/share-client.tsx +++ b/app/share/[id]/share-client.tsx @@ -31,11 +31,14 @@ import { import { Spinner } from "@/components/ui/spinner"; import { Textarea } from "@/components/ui/textarea"; import { MAX_SHARE_COMMENT_LENGTH } from "@/lib/share/limits"; +import { getShareContentForClipboard } from "@/lib/share/copy"; export function CopyShareActions() { async function copyText() { - const content = document.getElementById("share-content")?.textContent; - if (content === undefined) return; + const content = getShareContentForClipboard((id) => + document.getElementById(id), + ); + if (content === null) return; await navigator.clipboard.writeText(content); toast.success("คัดลอกข้อความแล้ว"); } diff --git a/lib/share/copy.ts b/lib/share/copy.ts new file mode 100644 index 0000000..ecbaee9 --- /dev/null +++ b/lib/share/copy.ts @@ -0,0 +1,5 @@ +export type ShareContentLookup = (id: string) => { textContent: string | null } | null; + +export function getShareContentForClipboard(lookup: ShareContentLookup) { + return lookup("share-content")?.textContent ?? null; +} diff --git a/lib/share/current-author.ts b/lib/share/current-author.ts index 6f2c9ba..b3d73bb 100644 --- a/lib/share/current-author.ts +++ b/lib/share/current-author.ts @@ -4,17 +4,16 @@ import { eq } from "drizzle-orm"; import { getServerSession } from "next-auth"; import { db } from "@/db"; import { users } from "@/db/schema"; -import { getSessionDiscordId, type SessionWithDiscord } from "@/lib/auth/auth"; +import type { SessionWithDiscord } from "@/lib/auth/auth"; import { authOptions } from "@/lib/auth/auth-options"; import { ShareHttpError } from "@/lib/share/http-error"; +import { requireShareSessionIdentity } from "@/lib/share/session"; export async function requireShareAuthor() { const session = await getServerSession(authOptions); - const discordId = getSessionDiscordId(session); - const userId = (session as SessionWithDiscord | null)?.user?.id; - if (!discordId || !userId) { - throw new ShareHttpError("Unauthorized", 401); - } + const { discordId, userId } = requireShareSessionIdentity( + session as SessionWithDiscord | null, + ); const [user] = await db .select({ id: users.id, name: users.name, image: users.image }) diff --git a/lib/share/session.ts b/lib/share/session.ts new file mode 100644 index 0000000..617d177 --- /dev/null +++ b/lib/share/session.ts @@ -0,0 +1,17 @@ +import { ShareHttpError } from "@/lib/share/http-error"; + +export type ShareSession = { + user?: { + id?: string | null; + discordId?: string | null; + } | null; +} | null; + +export function requireShareSessionIdentity(session: ShareSession) { + const userId = session?.user?.id; + const discordId = session?.user?.discordId; + if (!userId || !discordId) { + throw new ShareHttpError("Unauthorized", 401); + } + return { userId, discordId }; +} diff --git a/lib/share/ui-contracts.test.ts b/lib/share/ui-contracts.test.ts new file mode 100644 index 0000000..b6d9da8 --- /dev/null +++ b/lib/share/ui-contracts.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, test } from "bun:test"; +import { getShareContentForClipboard } from "@/lib/share/copy"; +import { requireShareSessionIdentity } from "@/lib/share/session"; + +describe("share UI contracts", () => { + test("copies exact share content without reading the description", () => { + const requestedIds: string[] = []; + const exactContent = " first line\r\n\tsecond line \n"; + const content = getShareContentForClipboard((id) => { + requestedIds.push(id); + if (id === "share-content") return { textContent: exactContent }; + if (id === "share-description") return { textContent: "description" }; + return null; + }); + + expect(content).toBe(exactContent); + expect(requestedIds).toEqual(["share-content"]); + }); + + test("rejects an unauthenticated web upload identity with 401", () => { + expect(() => requireShareSessionIdentity(null)).toThrow("Unauthorized"); + try { + requireShareSessionIdentity({ user: { id: "user-1" } }); + throw new Error("Expected a missing Discord ID to fail"); + } catch (error) { + expect(error).toMatchObject({ status: 401 }); + } + expect( + requireShareSessionIdentity({ + user: { id: "user-1", discordId: "123456789012345678" }, + }), + ).toEqual({ userId: "user-1", discordId: "123456789012345678" }); + }); + + test("keeps the extracted login dialog non-dismissible and shared", async () => { + const [dialog, sharePage, formPage] = await Promise.all([ + Bun.file(new URL("../../components/login-dialog.tsx", import.meta.url)).text(), + Bun.file(new URL("../../app/share/page.tsx", import.meta.url)).text(), + Bun.file(new URL("../../app/form/[id]/page.tsx", import.meta.url)).text(), + ]); + + expect(dialog).toContain("เข้าสู่ระบบก่อนกรอกแบบฟอร์ม"); + expect(dialog).toContain("showCloseButton={false}"); + expect(dialog).toContain("onEscapeKeyDown"); + expect(dialog).toContain("onInteractOutside"); + expect(dialog).toContain("callbackUrl: window.location.href"); + expect(sharePage).toContain('from "@/components/login-dialog"'); + expect(sharePage).toContain("if (!discordId)"); + expect(sharePage.indexOf("if (!discordId)")).toBeLessThan( + sharePage.indexOf(""), + ); + expect(formPage).toContain('from "@/components/login-dialog"'); + }); +});