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.
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
matchCandidate,
|
||||
dismissCandidates,
|
||||
resubmitRejected,
|
||||
resubmitRejectedWithDownload,
|
||||
exportInboxCsvUrl,
|
||||
} from "./inbox-api";
|
||||
|
||||
@@ -68,4 +69,58 @@ describe("inbox-api", () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -142,7 +142,6 @@ export async function resubmitRejected(
|
||||
export async function resubmitRejectedWithDownload(
|
||||
claimIds: string[],
|
||||
): Promise<{ blob: Blob; filename: string; serializeErrors: Array<{ claim_id: string; reason: string }> }> {
|
||||
if (!isConfigured) throw notConfiguredError();
|
||||
const res = await fetch(
|
||||
joinUrl("/api/inbox/rejected/resubmit?download=true"),
|
||||
{
|
||||
@@ -152,7 +151,7 @@ export async function resubmitRejectedWithDownload(
|
||||
},
|
||||
);
|
||||
if (!res.ok) {
|
||||
const detail = await readErrorBody(res);
|
||||
const detail = await res.text().catch(() => "");
|
||||
throw new Error(
|
||||
`${res.status} ${res.statusText}${detail ? ` — ${detail}` : ""}`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user