import { describe, expect, it, vi } from "vitest"; import { NextRequest } from "next/server"; import { POST } from "@/app/api/posts/route"; import { createImagePost, createVideoPost } from "@/lib/post-media"; import { createSession } from "@/lib/session-store"; // Server-side image-post pipeline, with a fully mocked feed. The browser sends // multipart content + one file to POST /api/posts; the server runs // draft -> presigned -> PUT -> confirm -> publish and returns only { 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(), }); } function jsonRes(body: unknown, status: number) { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); } function multipart(content: string, file: File | null, sid?: string) { const form = new FormData(); form.append("content", content); if (file) form.append("file", file, file.name); return new NextRequest("http://localhost/api/posts", { method: "POST", headers: sid ? { cookie: `mfb_session=${sid}` } : {}, body: form, }); } async function withFetch(fn: typeof fetch, run: () => Promise) { const orig = globalThis.fetch; globalThis.fetch = fn; try { return await run(); } finally { globalThis.fetch = orig; } } const imageFile = (name = "photo.jpg", type = "image/jpeg") => new File([new Uint8Array([1, 2, 3])], name, { type }); describe("POST /api/posts (image)", () => { it("runs draft -> presigned -> PUT -> confirm -> publish and returns only the id", async () => { const sid = session(); const calls: { url: string; method: string; auth: string | null }[] = []; const bodies: Record = {}; const fn = vi.fn(async (url: string | URL, init?: RequestInit) => { const u = String(url); calls.push({ url: u, method: String(init?.method ?? "GET"), auth: new Headers(init?.headers).get("authorization") }); if (u.includes("/api/v1/") && typeof init?.body === "string") { bodies[new URL(u).pathname] = JSON.parse(init.body); } if (u.endsWith("/api/v1/posts")) return jsonRes({ data: { id: "p9", status: "draft" } }, 201); if (u.endsWith("/attachments/presigned-urls")) return jsonRes({ data: [{ attachmentId: "a1", s3Key: "k/1.jpg", url: "https://s3.example/put?sig=1" }] }, 200); if (u === "https://s3.example/put?sig=1") return new Response(null, { status: 200 }); if (u.endsWith("/attachments/confirm-uploads")) return jsonRes({ data: { success: true } }, 200); if (u.includes("/publish")) return jsonRes({ data: { success: true } }, 200); return new Response("{}", { status: 404 }); }); const res = await withFetch(fn as unknown as typeof fetch, () => POST(multipart("look at this", imageFile(), sid)), ); expect(res.status).toBe(200); expect(await res.json()).toEqual({ id: "p9" }); const feedPaths = calls.filter((c) => c.url.includes("/api/v1/")).map((c) => new URL(c.url).pathname); expect(feedPaths).toEqual([ "/api/v1/posts", "/api/v1/attachments/presigned-urls", "/api/v1/attachments/confirm-uploads", "/api/v1/posts/p9/publish", ]); expect(bodies["/api/v1/posts"]).toEqual({ content: "look at this", type: "image", status: "draft", visibility: "public", }); expect(bodies["/api/v1/attachments/presigned-urls"]).toEqual({ postId: "p9", files: [{ name: "photo.jpg", type: "image/jpeg", size: 3 }], }); expect(bodies["/api/v1/attachments/confirm-uploads"]).toEqual({ postId: "p9", uploads: [{ attachmentId: "a1", s3Key: "k/1.jpg", mediaType: "image", filename: "photo.jpg" }], }); expect(bodies["/api/v1/posts/p9/publish"]).toEqual({ content: "look at this" }); // The server PUTs the bytes with no token; every feed call carries Bearer T. const put = calls.find((c) => c.method === "PUT"); expect(put?.url).toBe("https://s3.example/put?sig=1"); expect(put?.auth).toBeNull(); expect(calls.filter((c) => c.url.includes("/api/v1/")).every((c) => c.auth === "Bearer T")).toBe(true); }); it("never returns the feed token to the browser", async () => { const sid = session(); const fn = vi.fn(async (url: string | URL) => { const u = String(url); if (u.endsWith("/api/v1/posts")) return jsonRes({ data: { id: "p9" } }, 201); if (u.endsWith("/attachments/presigned-urls")) return jsonRes({ data: [{ attachmentId: "a1", s3Key: "k", url: "https://s3.example/put" }] }, 200); if (u === "https://s3.example/put") return new Response(null, { status: 200 }); return jsonRes({ data: { success: true } }, 200); }); const res = await withFetch(fn as unknown as typeof fetch, () => POST(multipart("hi", imageFile(), sid))); const text = await res.text(); expect(text).toBe(JSON.stringify({ id: "p9" })); expect(text).not.toContain("T"); }); it("rejects a disallowed type with 400 bad_file and makes no feed call", async () => { const sid = session(); const fn = vi.fn(); const res = await withFetch(fn as unknown as typeof fetch, () => POST(multipart("x", imageFile("doc.pdf", "application/pdf"), sid)), ); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: "bad_file" }); expect(fn).not.toHaveBeenCalled(); }); it("rejects a missing file with 400 bad_file", async () => { const sid = session(); const fn = vi.fn(); const res = await withFetch(fn as unknown as typeof fetch, () => POST(multipart("x", null, sid))); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: "bad_file" }); expect(fn).not.toHaveBeenCalled(); }); it("401 sign_in for a multipart post without a session", async () => { const fn = vi.fn(); const res = await withFetch(fn as unknown as typeof fetch, () => POST(multipart("x", imageFile()))); expect(res.status).toBe(401); expect(await res.json()).toEqual({ error: "sign_in" }); expect(fn).not.toHaveBeenCalled(); }); it("502 post_failed when the server upload PUT fails after the draft was created", async () => { const sid = session(); const fn = vi.fn(async (url: string | URL) => { const u = String(url); if (u.endsWith("/api/v1/posts")) return jsonRes({ data: { id: "p9" } }, 201); if (u.endsWith("/attachments/presigned-urls")) return jsonRes({ data: [{ attachmentId: "a1", s3Key: "k", url: "https://s3.example/put" }] }, 200); if (u === "https://s3.example/put") return new Response("denied", { status: 403 }); return jsonRes({ data: { success: true } }, 200); }); const res = await withFetch(fn as unknown as typeof fetch, () => POST(multipart("x", imageFile(), sid))); expect(res.status).toBe(502); expect(await res.json()).toEqual({ error: "post_failed" }); }); it("accepts every allowed image type and rejects others (unit)", async () => { const sid = session(); const ok = vi.fn(async (url: string | URL) => { const u = String(url); if (u.endsWith("/api/v1/posts")) return jsonRes({ data: { id: "p9" } }, 201); if (u.endsWith("/attachments/presigned-urls")) return jsonRes({ data: [{ attachmentId: "a1", s3Key: "k", url: "https://s3.example/put" }] }, 200); if (u === "https://s3.example/put") return new Response(null, { status: 200 }); return jsonRes({ data: { success: true } }, 200); }) as unknown as typeof fetch; for (const type of ["image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", "image/heic"]) { const r = await createImagePost(sid, "c", imageFile(`f.${type.split("/")[1]}`, type), { fetchImpl: ok }); expect(r.ok).toBe(true); } const bad = await createImagePost(sid, "c", imageFile("a.svg", "image/svg+xml"), { fetchImpl: (() => { throw new Error("should not be called"); }) as unknown as typeof fetch, }); expect(bad).toEqual({ ok: false, status: 400, error: "bad_file" }); }); }); describe("POST /api/posts (video)", () => { const videoFile = (name = "clip.mp4", type = "video/mp4") => new File([new Uint8Array([1, 2, 3])], name, { type }); function videoFeed(bodies: Record, calls: string[]) { return vi.fn(async (url: string | URL, init?: RequestInit) => { const u = String(url); calls.push(u); if (u.includes("/api/v1/") && typeof init?.body === "string") bodies[new URL(u).pathname] = JSON.parse(init.body); if (u.endsWith("/api/v1/posts")) return jsonRes({ data: { id: "v1", status: "draft" } }, 201); if (u.endsWith("/attachments/presigned-urls")) return jsonRes({ data: [{ attachmentId: "a1", s3Key: "k/1.mp4", url: "https://s3.example/put" }] }, 200); if (u === "https://s3.example/put") return new Response(null, { status: 200 }); // Confirmation returns while transcoding continues: no hlsUrl. if (u.endsWith("/confirm-uploads")) return jsonRes({ data: { status: "processing" } }, 200); return jsonRes({ data: { success: true } }, 200); }); } it("uses draft type video, confirms mediaType video, and publishes without hlsUrl", async () => { const sid = session(); const bodies: Record = {}; const calls: string[] = []; const fn = videoFeed(bodies, calls); const res = await withFetch(fn as unknown as typeof fetch, () => POST(multipart("hi", videoFile("c.mov", "video/quicktime"), sid))); expect(res.status).toBe(200); expect(await res.json()).toEqual({ id: "v1" }); expect((bodies["/api/v1/posts"] as { type: string }).type).toBe("video"); const uploads = (bodies["/api/v1/attachments/confirm-uploads"] as { uploads: { mediaType: string }[] }).uploads; expect(uploads[0].mediaType).toBe("video"); expect(calls.some((u) => u.endsWith("/api/v1/posts/v1/publish"))).toBe(true); }); it("accepts mp4, quicktime and webm; rejects other video types before any feed call", async () => { const sid = session(); const ok = videoFeed({}, []) as unknown as typeof fetch; for (const t of ["video/mp4", "video/quicktime", "video/webm"]) { expect((await createVideoPost(sid, "c", videoFile("f", t), { fetchImpl: ok })).ok).toBe(true); } const fn = vi.fn(); const res = await withFetch(fn as unknown as typeof fetch, () => POST(multipart("x", videoFile("a.avi", "video/x-msvideo"), sid)), ); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: "bad_file" }); expect(fn).not.toHaveBeenCalled(); }); });