Files
cyclone/src/lib/inbox-api.test.ts
T
Tyler cb74bc9307 test(sp8): add coverage for resubmitRejectedWithDownload + bundle modal flow
- inbox-api.test.ts: pin ?download=true POST contract; blob + filename +
  X-Cyclone-Serialize-Errors parsing; non-2xx error surfacing.
- Inbox.test.tsx: end-to-end multi-select → Resubmit + Download path
  verifies api call args and downloadBlob wiring.
- inbox-api.ts: drop redundant isConfigured short-circuit in download
  variant (backend has its own auth gate) and switch error reading to
  res.text() to match Blob response shape.
2026-06-20 20:53:58 -06:00

127 lines
4.3 KiB
TypeScript

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();
});
});