import { describe, expect, it, vi } from "vitest"; import { fetchInboxLanes, matchCandidate, dismissCandidates, resubmitRejected, resubmitRejectedWithDownload, exportInboxCsvUrl, } from "./inbox-api"; const BASE = "http://test.local"; describe("inbox-api", () => { it("fetchInboxLanes hits /api/inbox/lanes", async () => { const fetchSpy = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ rejected: [], candidates: [], unmatched: [], done_today: [] }), }); vi.stubGlobal("fetch", fetchSpy); const lanes = await fetchInboxLanes(); expect(fetchSpy).toHaveBeenCalledWith(`${BASE}/api/inbox/lanes`); expect(lanes.rejected).toEqual([]); vi.unstubAllGlobals(); }); it("matchCandidate posts to the candidates/{remit_id}/match endpoint", async () => { const fetchSpy = vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) }); vi.stubGlobal("fetch", fetchSpy); await matchCandidate("R-1", "C-1"); expect(fetchSpy).toHaveBeenCalledWith( `${BASE}/api/inbox/candidates/R-1/match`, expect.objectContaining({ method: "POST", body: JSON.stringify({ claim_id: "C-1" }), }), ); vi.unstubAllGlobals(); }); it("dismissCandidates posts pairs", async () => { const fetchSpy = vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) }); vi.stubGlobal("fetch", fetchSpy); await dismissCandidates([{ claim_id: "C-1", remit_id: "R-1" }]); expect(fetchSpy).toHaveBeenCalledWith( `${BASE}/api/inbox/candidates/dismiss`, expect.objectContaining({ method: "POST", body: JSON.stringify({ pairs: [{ claim_id: "C-1", remit_id: "R-1" }] }), }), ); vi.unstubAllGlobals(); }); it("resubmitRejected posts claim_ids", async () => { const fetchSpy = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ ok: true, resubmitted: ["C-1"], conflicts: [] }), }); vi.stubGlobal("fetch", fetchSpy); const r = await resubmitRejected(["C-1"]); expect(fetchSpy).toHaveBeenCalledWith( `${BASE}/api/inbox/rejected/resubmit`, expect.objectContaining({ method: "POST" }), ); expect(r.resubmitted).toEqual(["C-1"]); vi.unstubAllGlobals(); }); it("exportInboxCsvUrl returns the export endpoint with the lane", () => { expect(exportInboxCsvUrl("rejected")).toBe(`${BASE}/api/inbox/export.csv?lane=rejected`); }); it("resubmitRejectedWithDownload posts to ?download=true and returns blob + filename", async () => { // The download path returns a Blob (zip) plus the suggested filename // from Content-Disposition, and surfaces per-claim serialize errors // via X-Cyclone-Serialize-Errors (JSON-encoded array). const zipBytes = new Uint8Array([0x50, 0x4b, 0x03, 0x04]); // PK header const fetchSpy = vi.fn().mockResolvedValue({ ok: true, blob: async () => new Blob([zipBytes], { type: "application/zip" }), headers: { get: (name: string) => { const lower = name.toLowerCase(); if (lower === "content-disposition") { return 'attachment; filename="resubmit-3-claims.zip"'; } if (lower === "x-cyclone-serialize-errors") { return JSON.stringify([{ claim_id: "C2", reason: "no raw_json" }]); } return null; }, }, }); vi.stubGlobal("fetch", fetchSpy); const out = await resubmitRejectedWithDownload(["C1", "C2", "C3"]); expect(fetchSpy).toHaveBeenCalledWith( `${BASE}/api/inbox/rejected/resubmit?download=true`, expect.objectContaining({ method: "POST", body: JSON.stringify({ claim_ids: ["C1", "C2", "C3"] }), }), ); expect(out.filename).toBe("resubmit-3-claims.zip"); expect(out.blob).toBeInstanceOf(Blob); expect(out.serializeErrors).toEqual([ { claim_id: "C2", reason: "no raw_json" }, ]); vi.unstubAllGlobals(); }); it("resubmitRejectedWithDownload surfaces non-2xx as an Error", async () => { const fetchSpy = vi.fn().mockResolvedValue({ ok: false, status: 400, statusText: "Bad Request", text: async () => "claim_ids required", headers: { get: () => null }, }); vi.stubGlobal("fetch", fetchSpy); await expect(resubmitRejectedWithDownload([])).rejects.toThrow(/400/); vi.unstubAllGlobals(); }); });