79 lines
3.3 KiB
TypeScript
79 lines
3.3 KiB
TypeScript
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 = '<meta name="csrf-token" content="csrf-token">';
|
|
const fetchMock = vi
|
|
.fn<typeof fetch>()
|
|
.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<typeof fetch>()
|
|
.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<typeof fetch>()
|
|
.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<typeof fetch>().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<typeof fetch>().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",
|
|
});
|
|
});
|
|
});
|