3dom4wf7p: Publish public text posts from a feed composer
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { NextRequest } from "next/server";
|
||||
import { POST } from "@/app/api/posts/route";
|
||||
import { Composer } from "@/components/Composer";
|
||||
import { FeedView } from "@/components/FeedView";
|
||||
import { PostError, publishAndConfirm, sendPost } from "@/lib/post-client";
|
||||
import { createSession } from "@/lib/session-store";
|
||||
|
||||
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(),
|
||||
});
|
||||
}
|
||||
const req = (body: unknown, sid?: string) =>
|
||||
new NextRequest("http://localhost/api/posts", {
|
||||
method: "POST",
|
||||
headers: sid ? { cookie: `mfb_session=${sid}`, "content-type": "application/json" } : { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
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 item = (id: string) => ({ type: "post", publishedAt: "2026-10-19T10:00:00Z", post: { id, content: "x", author: {} } });
|
||||
|
||||
describe("POST /api/posts", () => {
|
||||
it("401 sign_in without a session", async () => {
|
||||
const res = await POST(req({ content: "hi" }));
|
||||
expect(res.status).toBe(401);
|
||||
expect(await res.json()).toEqual({ error: "sign_in" });
|
||||
});
|
||||
|
||||
it("creates a public text draft, then publishes it with the same content", async () => {
|
||||
const sid = session();
|
||||
const calls: { path: string; body: unknown; auth: string | null }[] = [];
|
||||
const fn = vi.fn(async (url: string, init?: RequestInit) => {
|
||||
calls.push({
|
||||
path: new URL(String(url)).pathname,
|
||||
body: JSON.parse(String(init?.body)),
|
||||
auth: new Headers(init?.headers).get("authorization"),
|
||||
});
|
||||
return new Response(JSON.stringify({ data: { id: "p9", status: "draft" } }), { status: 201 });
|
||||
});
|
||||
const res = await withFetch(fn as unknown as typeof fetch, () => POST(req({ content: "hello" }, sid)));
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({ id: "p9" });
|
||||
expect(calls.map((c) => c.path)).toEqual(["/api/v1/posts", "/api/v1/posts/p9/publish"]);
|
||||
expect(calls[0].body).toEqual({ content: "hello", type: "text", status: "draft", visibility: "public" });
|
||||
expect(calls[1].body).toEqual({ content: "hello" });
|
||||
expect(calls.every((c) => c.auth === "Bearer T")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects whitespace-only content without calling the feed", async () => {
|
||||
const sid = session();
|
||||
const fn = vi.fn();
|
||||
const res = await withFetch(fn as unknown as typeof fetch, () => POST(req({ content: " \n " }, sid)));
|
||||
expect(res.status).toBe(400);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("502 post_failed when publish fails", async () => {
|
||||
const sid = session();
|
||||
let n = 0;
|
||||
const fn = vi.fn(async () =>
|
||||
n++ === 0 ? new Response(JSON.stringify({ data: { id: "p9" } }), { status: 201 }) : new Response("{}", { status: 500 }),
|
||||
);
|
||||
const res = await withFetch(fn as unknown as typeof fetch, () => POST(req({ content: "hello" }, sid)));
|
||||
expect(res.status).toBe(502);
|
||||
expect(await res.json()).toEqual({ error: "post_failed" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("publishAndConfirm", () => {
|
||||
it("returns the reloaded page only when the id is on the first page", async () => {
|
||||
const state = { id: null as string | null };
|
||||
const page = { items: [item("p9"), item("p1")], nextCursor: null };
|
||||
const got = await publishAndConfirm("hi", state, { send: async () => "p9", fetchPage: async () => page });
|
||||
expect(got).toBe(page);
|
||||
expect(state.id).toBeNull();
|
||||
});
|
||||
|
||||
it("throws and keeps the id when the post is not on the page, then a retry does not create again", async () => {
|
||||
const state = { id: null as string | null };
|
||||
const send = vi.fn(async () => "p9");
|
||||
const miss = async () => ({ items: [item("p1")], nextCursor: null });
|
||||
await expect(publishAndConfirm("hi", state, { send, fetchPage: miss })).rejects.toBeInstanceOf(PostError);
|
||||
expect(state.id).toBe("p9");
|
||||
const hit = async () => ({ items: [item("p9")], nextCursor: null });
|
||||
await publishAndConfirm("hi", state, { send, fetchPage: hit });
|
||||
expect(send).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("sendPost maps 401 to signed out and other failures to publish_failed", async () => {
|
||||
const f = (status: number) => (async () => new Response("{}", { status })) as unknown as typeof fetch;
|
||||
await expect(sendPost("x", f(401))).rejects.toMatchObject({ signedOut: true });
|
||||
await expect(sendPost("x", f(500))).rejects.toMatchObject({ signedOut: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Composer", () => {
|
||||
const base = { value: "", busy: false, error: false, onChange: () => {}, onSubmit: () => {} };
|
||||
it("disables Post for empty or whitespace-only text", () => {
|
||||
expect(renderToStaticMarkup(createElement(Composer, base))).toMatch(/<button[^>]*disabled/);
|
||||
expect(renderToStaticMarkup(createElement(Composer, { ...base, value: " " }))).toMatch(/<button[^>]*disabled/);
|
||||
expect(renderToStaticMarkup(createElement(Composer, { ...base, value: "hi" }))).not.toMatch(/<button[^>]*disabled/);
|
||||
});
|
||||
it("names the action on failure and keeps the text", () => {
|
||||
const html = renderToStaticMarkup(createElement(Composer, { ...base, value: "my draft", error: true }));
|
||||
expect(html).toContain("Publishing the post failed");
|
||||
expect(html).toContain("my draft");
|
||||
});
|
||||
it("is shown in an empty feed", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
createElement(FeedView, {
|
||||
posts: [], status: "ready", loadingMore: false, loadMoreFailed: false, newCount: 0,
|
||||
onRetry: () => {}, onShowNew: () => {}, composer: base,
|
||||
}),
|
||||
);
|
||||
expect(html).toContain("No posts yet.");
|
||||
expect(html).toContain("composer-text");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user