bo5tym52p: List and create comments on post cards
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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<T>(fn: typeof fetch, run: () => Promise<T>) {
|
||||
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(/<button type="submit" disabled/);
|
||||
const on = renderToStaticMarkup(createElement(CommentsPanel, { comments: [], value: "hi" }));
|
||||
expect(on).not.toMatch(/<button type="submit" disabled/);
|
||||
});
|
||||
|
||||
it("an error names the action and the list stays", () => {
|
||||
const html = renderToStaticMarkup(createElement(CommentsPanel, { comments: toComments(raw), value: "x", error: commentErrorText("post") }));
|
||||
expect(html).toContain("Could not post comment.");
|
||||
expect(html).toContain("Ana Ruiz");
|
||||
});
|
||||
});
|
||||
|
||||
describe("comment client", () => {
|
||||
const proxy = (comments: unknown[]) => vi.fn(async (_u: string, init?: RequestInit) => {
|
||||
if (init?.method === "POST") {
|
||||
const made = { key: "c9", id: "c9", authorName: "Me", createdAt: "2026-10-19T12:00:00Z", removed: false, content: JSON.parse(String(init.body)).content };
|
||||
return new Response(JSON.stringify({ comment: made }), { status: 200 });
|
||||
}
|
||||
return new Response(JSON.stringify({ comments }), { status: 200 });
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
it("posting adds the new comment to the list", async () => {
|
||||
const f = proxy([]);
|
||||
const list = await loadComments("p1", f);
|
||||
const next = await postComment("p1", " new one ", toComments(raw).concat(list), f);
|
||||
expect(next).toHaveLength(3);
|
||||
expect(next[2]).toMatchObject({ id: "c9", content: "new one" });
|
||||
});
|
||||
|
||||
it("empty content does not submit", async () => {
|
||||
const f = proxy([]);
|
||||
const cur = toComments(raw);
|
||||
expect(await postComment("p1", " ", cur, f)).toBe(cur);
|
||||
expect(f).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("a failed post throws naming the action; a 401 marks signed out", async () => {
|
||||
const f = vi.fn(async () => new Response("{}", { status: 502 })) as unknown as typeof fetch;
|
||||
await expect(postComment("p1", "x", [], f)).rejects.toMatchObject({ action: "post", signedOut: false });
|
||||
const out = vi.fn(async () => new Response("{}", { status: 401 })) as unknown as typeof fetch;
|
||||
await expect(loadComments("p1", out)).rejects.toBeInstanceOf(CommentError);
|
||||
await expect(loadComments("p1", out)).rejects.toMatchObject({ action: "load", signedOut: true });
|
||||
expect(commentErrorText("load")).toBe("Could not load comments.");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user