import { describe, expect, it, vi } from "vitest"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { NextRequest } from "next/server"; import { GET, POST } from "@/app/api/posts/[id]/comments/route"; import { CommentsPanel } from "@/components/CommentsPanel"; import { CommentError, commentErrorText, loadComments, postComment } from "@/lib/comment-client"; import { toComments } from "@/lib/comment-model"; import { createSession } from "@/lib/session-store"; const ctx = (id: string) => ({ params: Promise.resolve({ id }) }); function session() { return createSession({ profileAccessToken: "p", profileAccessTokenExpiresAt: null, profileRefreshToken: "r", feedAccessToken: "T", feedAccessTokenExpiresAt: null, person: { userId: "u", name: "A", email: "a@b.co", pictureUrl: null }, createdAt: Date.now(), }); } async function withFetch(fn: typeof fetch, run: () => Promise) { const orig = globalThis.fetch; globalThis.fetch = fn; try { return await run(); } finally { globalThis.fetch = orig; } } const raw = [ { id: "c1", content: "first", createdAt: "2026-10-19T10:00:00Z", author: { displayName: "Ana Ruiz" } }, { id: "c2", content: "SECRET OLD TEXT", createdAt: "2026-10-19T10:05:00Z", deletedAt: "2026-10-19T11:00:00Z", author: { firstName: "Bo", lastName: "Li" } }, ]; describe("comment routes", () => { it("401 sign_in without a session", async () => { const g = await GET(new NextRequest("http://localhost/api/posts/p1/comments"), ctx("p1")); expect(g.status).toBe(401); expect(await g.json()).toEqual({ error: "sign_in" }); const p = await POST(new NextRequest("http://localhost/x", { method: "POST", body: JSON.stringify({ content: "hi" }) }), ctx("p1")); expect(p.status).toBe(401); expect(await p.json()).toEqual({ error: "sign_in" }); }); it("GET reads the feed list with limit and page; removed comments lose their text", async () => { const sid = session(); const urls: string[] = []; const f = vi.fn(async (u: string) => { urls.push(String(u)); return new Response(JSON.stringify({ data: { comments: raw } }), { status: 200 }); }); await withFetch(f as unknown as typeof fetch, async () => { const r = await GET(new NextRequest("http://localhost/x", { headers: { cookie: `mfb_session=${sid}` } }), ctx("p1")); const body = await r.json(); expect(body.comments).toHaveLength(2); expect(body.comments[0]).toMatchObject({ content: "first", authorName: "Ana Ruiz" }); expect(body.comments[1]).toMatchObject({ removed: true, content: "", authorName: "Bo Li" }); expect(JSON.stringify(body)).not.toContain("SECRET OLD TEXT"); }); expect(urls[0]).toMatch(/\/api\/v1\/posts\/p1\/comments\?limit=20&page=1$/); }); it("POST sends the text comment payload and rejects empty content without calling the feed", async () => { const sid = session(); const h = { cookie: `mfb_session=${sid}` }; const sent: unknown[] = []; const f = vi.fn(async (_u: string, init?: RequestInit) => { sent.push(JSON.parse(String(init?.body))); return new Response(JSON.stringify({ data: { id: "c9", content: "hello", createdAt: "2026-10-19T12:00:00Z", author: { displayName: "Me" } } }), { status: 201 }); }); await withFetch(f as unknown as typeof fetch, async () => { const ok = await POST(new NextRequest("http://localhost/x", { method: "POST", headers: h, body: JSON.stringify({ content: "hello" }) }), ctx("p1")); expect((await ok.json()).comment).toMatchObject({ id: "c9", content: "hello" }); const empty = await POST(new NextRequest("http://localhost/x", { method: "POST", headers: h, body: JSON.stringify({ content: " " }) }), ctx("p1")); expect(empty.status).toBe(400); }); expect(sent).toEqual([{ content: "hello", type: "text", status: "published" }]); expect(f).toHaveBeenCalledTimes(1); }); it("a feed failure is 502 with the action in the error", async () => { const sid = session(); const h = { cookie: `mfb_session=${sid}` }; const bad = vi.fn(async () => new Response("{}", { status: 500 })) as unknown as typeof fetch; await withFetch(bad, async () => { const g = await GET(new NextRequest("http://localhost/x", { headers: h }), ctx("p1")); expect([g.status, await g.json()]).toEqual([502, { error: "comments_unavailable" }]); const p = await POST(new NextRequest("http://localhost/x", { method: "POST", headers: h, body: JSON.stringify({ content: "x" }) }), ctx("p1")); expect([p.status, await p.json()]).toEqual([502, { error: "comment_failed" }]); }); }); }); describe("comments panel", () => { it("opening lists comments with author and time; a removed one shows no old text", () => { const html = renderToStaticMarkup(createElement(CommentsPanel, { comments: toComments(raw), value: "", now: Date.parse("2026-10-19T12:00:00Z") })); expect(html).toContain("Ana Ruiz"); expect(html).toContain("first"); expect(html).toContain("2h ago"); expect(html).toContain("This comment was removed."); expect(html).not.toContain("SECRET OLD TEXT"); }); it("empty or whitespace input disables submit", () => { const off = renderToStaticMarkup(createElement(CommentsPanel, { comments: [], value: " " })); expect(off).toMatch(/