test: cover share authentication and copy behavior
This commit is contained in:
@@ -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("คัดลอกข้อความแล้ว");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export type ShareContentLookup = (id: string) => { textContent: string | null } | null;
|
||||
|
||||
export function getShareContentForClipboard(lookup: ShareContentLookup) {
|
||||
return lookup("share-content")?.textContent ?? null;
|
||||
}
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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("<ShareUploadClient />"),
|
||||
);
|
||||
expect(formPage).toContain('from "@/components/login-dialog"');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user