import { afterEach, describe, expect, it, vi } from "vitest"; import { uploadFileInChunks, waitForChatAttachmentScan } from "./uploadFileClient"; afterEach(() => { vi.unstubAllGlobals(); document.head.replaceChildren(); }); describe("uploadFileInChunks", () => { it("sends the session CSRF token with every upload mutation", async () => { document.head.innerHTML = ''; const fetchMock = vi .fn() .mockResolvedValueOnce(Response.json({ uploadId: "upload_1", chunkSize: 4, totalChunks: 1 })) .mockResolvedValueOnce(Response.json({ receivedChunks: 1, totalChunks: 1 })) .mockResolvedValueOnce(Response.json({ id: "file_1", displayName: "pilot.txt" })); vi.stubGlobal("fetch", fetchMock); await uploadFileInChunks({ tenantSlug: "demo", file: new File(["test"], "pilot.txt", { type: "text/plain" }), }); expect(fetchMock).toHaveBeenCalledTimes(3); for (const [, init] of fetchMock.mock.calls) { expect(init?.headers).toMatchObject({ "x-csrf-token": "csrf-token" }); } }); }); describe("waitForChatAttachmentScan", () => { it("polls pending scans until the owner-only endpoint reports clean", async () => { const fetchImpl = vi .fn() .mockResolvedValueOnce(Response.json({ scanStatus: "pending" })) .mockResolvedValueOnce(Response.json({ scanStatus: "clean" })); await expect(waitForChatAttachmentScan({ tenantSlug: "demo", fileId: "file_1", fetchImpl, pollIntervalMs: 0, maxAttempts: 2 })).resolves.toBeUndefined(); expect(fetchImpl).toHaveBeenNthCalledWith(1, "/demo/chat/attachments/file_1/status", expect.objectContaining({ headers: { accept: "application/json" } })); }); it("removes each abort listener after a completed polling delay", async () => { const controller = new AbortController(); const add = vi.spyOn(controller.signal, "addEventListener"); const remove = vi.spyOn(controller.signal, "removeEventListener"); const fetchImpl = vi .fn() .mockResolvedValueOnce(Response.json({ scanStatus: "pending" })) .mockResolvedValueOnce(Response.json({ scanStatus: "clean" })); await waitForChatAttachmentScan({ tenantSlug: "demo", fileId: "file_1", fetchImpl, signal: controller.signal, pollIntervalMs: 1, maxAttempts: 2, }); expect(add).toHaveBeenCalledOnce(); expect(remove).toHaveBeenCalledWith("abort", expect.any(Function)); }); it.each(["infected", "skipped"])("fails closed when the scan result is %s", async (scanStatus) => { const fetchImpl = vi.fn().mockResolvedValue(Response.json({ scanStatus })); await expect(waitForChatAttachmentScan({ tenantSlug: "demo", fileId: "file_1", fetchImpl, pollIntervalMs: 0, maxAttempts: 1 })).rejects.toMatchObject({ errorKey: "chat.attachments.blocked", }); }); it("returns a clear retry error when scanning stays pending", async () => { const fetchImpl = vi.fn().mockImplementation(async () => Response.json({ scanStatus: "pending" })); await expect(waitForChatAttachmentScan({ tenantSlug: "demo", fileId: "file_1", fetchImpl, pollIntervalMs: 0, maxAttempts: 2 })).rejects.toMatchObject({ errorKey: "chat.attachments.scanPendingRetry", }); }); });