import { afterEach, describe, expect, it, vi } from "vitest"; import { fetchDraft, fetchOutline } from "./server"; // 纯加载/降级逻辑(mock fetch,无网络/DOM):大纲解包/降级、草稿 404→null。 function mockFetch(status: number, body: unknown): void { vi.stubGlobal( "fetch", vi.fn(async () => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" }, }), ), ); } afterEach(() => { vi.unstubAllGlobals(); }); describe("fetchOutline", () => { it("returns the persisted chapters on 200", async () => { mockFetch(200, { chapters: [{ no: 1, volume: 1, beats: ["开场"] }] }); const chapters = await fetchOutline("p1"); expect(chapters).toHaveLength(1); expect(chapters[0]?.no).toBe(1); }); it("returns empty array when the project has no outline", async () => { mockFetch(200, { chapters: [] }); expect(await fetchOutline("p1")).toEqual([]); }); it("falls back to empty array on any error (does not throw)", async () => { mockFetch(404, { error: { code: "NOT_FOUND", message: "no project" } }); expect(await fetchOutline("missing")).toEqual([]); }); }); describe("fetchDraft", () => { it("returns the saved draft view on 200", async () => { mockFetch(200, { project_id: "p1", chapter_no: 1, volume: 1, status: "draft", version: 2, content: "第一章正文", length: 5, }); const draft = await fetchDraft("p1", 1); expect(draft?.content).toBe("第一章正文"); expect(draft?.version).toBe(2); }); it("returns null when no draft exists (404 → empty editor)", async () => { mockFetch(404, { error: { code: "NOT_FOUND", message: "no draft" } }); expect(await fetchDraft("p1", 9)).toBeNull(); }); }); // parseJsonBody 边界校验:防御被代理/网关污染的非对象或非 JSON 响应。 describe("parseJsonBody structural validation", () => { it("throws on a 200 body that is not a JSON object (primitive)", async () => { mockFetch(200, "surprise-string"); await expect(fetchDraft("p1", 1)).rejects.toThrow("响应体形状异常"); }); it("throws when the 200 body is not valid JSON", async () => { vi.stubGlobal( "fetch", vi.fn(async () => new Response("not json", { status: 200 })), ); await expect(fetchDraft("p1", 1)).rejects.toThrow("响应非 JSON"); }); });