import { afterEach, beforeEach, expect, it, vi } from "vitest"; import { api, apiRaw, formatPageUrl, getToken, HttpError, setToken } from "../src/api/client"; const jres = (status: number, body: unknown) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); beforeEach(() => setToken(null)); afterEach(() => vi.unstubAllGlobals()); it("login 成功存 token,请求体带凭据", async () => { const f = vi.fn(async (_url: any, _init: any) => jres(200, { token: "T" })); vi.stubGlobal("fetch", f); await expect(api.login("a", "b")).resolves.toEqual({ token: "T" }); expect(f.mock.calls[0][0]).toBe("/api/auth/login"); expect(String(f.mock.calls[0][1].body)).toContain('"username":"a"'); }); it("401 清 token 并抛 HttpError(parsed code)", async () => { setToken("T"); vi.stubGlobal("fetch", async (_url: any, init: any) => { expect(init.headers.Authorization).toBe("Bearer T"); return jres(401, { error: { code: "unauthorized", message: "bad credentials" } }); }); const e = await api.login("a", "b").catch((e) => e); expect(e).toBeInstanceOf(HttpError); expect((e as HttpError).code).toBe("unauthorized"); expect(getToken()).toBeNull(); }); it("非 JSON 错误体回退 http_", async () => { vi.stubGlobal("fetch", async () => new Response("oops", { status: 500 })); const e = await api.listUsers().catch((e) => e); expect((e as HttpError).code).toBe("http_500"); }); it("204 无体 resolve undefined", async () => { vi.stubGlobal("fetch", async () => new Response(null, { status: 204 })); await expect(api.deleteBook(1)).resolves.toBeUndefined(); }); it("相对路径补 /api 前缀,绝对 /api 原样;query 拼装", async () => { const seen: string[] = []; vi.stubGlobal("fetch", async (url: any) => { seen.push(String(url)); return jres(200, []); }); await api.listBooks({ library: 3, q: "x" }); await apiRaw("/api/books/1/cover?v=h"); expect(seen).toEqual(["/api/books?library=3&q=x", "/api/books/1/cover?v=h"]); }); it("page_url_fmt %d 替换", () => { expect(formatPageUrl("/api/books/9/pages/%d?v=h", 12)).toBe("/api/books/9/pages/12?v=h"); }); it("putProgress keepalive 透传 + JSON body", async () => { const f = vi.fn(async (_url: any, _init: any) => new Response(null, { status: 204 })); vi.stubGlobal("fetch", f); await api.putProgress(1, { page: 2 }, 0.5, true); expect(f.mock.calls[0][1].keepalive).toBe(true); expect(String(f.mock.calls[0][1].body)).toContain('"percent":0.5'); });