import { afterEach, describe, expect, it, vi } from "vitest"; import { hasOpenCredentials, openApiBase, openCommentXml, openHeaders, openMatch, readOpenCredentials, } from "./client.js"; afterEach(() => { vi.unstubAllEnvs(); vi.unstubAllGlobals(); }); describe("readOpenCredentials", () => { it("reads from injected env", () => { expect( readOpenCredentials({ OPEN_DANMAKU_APP_ID: "id", OPEN_DANMAKU_APP_SECRET: "s" }), ).toEqual({ appId: "id", appSecret: "s", }); }); it("returns null when missing", () => { expect(readOpenCredentials({})).toBeNull(); expect(readOpenCredentials({ OPEN_DANMAKU_APP_ID: "id" })).toBeNull(); }); it("defaults to process.env", () => { vi.stubEnv("OPEN_DANMAKU_APP_ID", "pid"); vi.stubEnv("OPEN_DANMAKU_APP_SECRET", "ps"); expect(readOpenCredentials()).toEqual({ appId: "pid", appSecret: "ps" }); expect(hasOpenCredentials()).toBe(true); }); }); describe("openApiBase", () => { it("strips trailing slashes", () => { vi.stubEnv("OPEN_DANMAKU_API_BASE", "https://example.test///"); expect(openApiBase()).toBe("https://example.test"); }); it("defaults to dandanplay api", () => { vi.stubEnv("OPEN_DANMAKU_API_BASE", undefined); expect(openApiBase()).toBe("https://api.dandanplay.net"); }); }); describe("openHeaders", () => { it("signs request when credentials present", () => { const h = openHeaders("/api/v2/match", { appId: "id", appSecret: "s" }); expect(h["Content-Type"]).toBe("application/json"); expect(h["X-AppId"]).toBe("id"); expect(h["X-Timestamp"]).toMatch(/^\d+$/); expect(h["X-Signature"]).toBeTruthy(); }); it("only content-type without credentials", () => { expect(openHeaders("/api/v2/match", null)).toEqual({ "Content-Type": "application/json" }); }); }); function stubFetchJson(json: unknown) { const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => json, }); vi.stubGlobal("fetch", fetchMock); return fetchMock; } describe("openCommentXml 请求契约", () => { it("请求路径含 ?withRelated=true(关联弹幕计入)", async () => { const fetchMock = stubFetchJson({ success: true, comments: [] }); await openCommentXml(555); expect(fetchMock).toHaveBeenCalledTimes(1); const url = String(fetchMock.mock.calls[0]?.[0]); expect(url).toBe("https://api.dandanplay.net/api/v2/comment/555?withRelated=true"); }); it("comments 序列化为 xml,HTTP 失败返回 null", async () => { const fetchMock = stubFetchJson({ success: true, comments: [{ p: "12.5,1,0,0,0,0,,", m: "hi" }], }); await expect(openCommentXml(555)).resolves.toContain(" ({}) }); await expect(openCommentXml(555)).resolves.toBeNull(); }); }); describe("openMatch 请求契约", () => { it("POST /api/v2/match,matchMode 随 hash 切换", async () => { const fetchMock = stubFetchJson({ success: true, isMatched: true, matches: [] }); await openMatch("Show - 02", 100, "abc"); const [url1, init1] = fetchMock.mock.calls[0] ?? []; expect(String(url1)).toBe("https://api.dandanplay.net/api/v2/match"); expect(init1?.method).toBe("POST"); expect(JSON.parse(String(init1?.body))).toEqual({ fileName: "Show - 02", fileHash: "abc", fileSize: 100, matchMode: "hashAndFileName", }); await openMatch("Show - 02", 100, null); const init2 = fetchMock.mock.calls[1]?.[1]; expect(JSON.parse(String(init2?.body))).toMatchObject({ fileHash: "", matchMode: "fileNameOnly", }); }); });