// @vitest-environment node import { beforeEach, describe, expect, it, vi } from "vitest"; const { authenticateRequest, createPendingAttachment, deleteGateFile, uploadGateFile } = vi.hoisted(() => ({ authenticateRequest: vi.fn(), createPendingAttachment: vi.fn(), deleteGateFile: vi.fn(), uploadGateFile: vi.fn(), })); vi.mock("server-only", () => ({})); vi.mock("@/lib/db/pool", () => ({ getPool: () => ({}) })); vi.mock("@/lib/auth/server", async (importOriginal) => ({ ...(await importOriginal()), authenticateRequest, })); vi.mock("@/lib/db/attachments", () => ({ createPendingAttachment })); vi.mock("@/lib/chat/gate-files", async (importOriginal) => ({ ...(await importOriginal()), deleteGateFile, uploadGateFile, })); import { POST } from "@/app/api/attachments/route"; import { AuthError } from "@/lib/auth/server"; beforeEach(() => { authenticateRequest.mockReset().mockResolvedValue({ id: "00000000-0000-4000-8000-000000000001", email: "alice@example.com", }); uploadGateFile.mockReset().mockResolvedValue({ id: "file-opaque_123", filename: "brief.txt", bytes: 5, }); deleteGateFile.mockReset().mockResolvedValue(undefined); createPendingAttachment.mockReset().mockResolvedValue({ id: "00000000-0000-4000-8000-000000000077", gateFileId: "file-opaque_123", objectKey: null, name: "brief.txt", contentType: "text/plain", size: 5, }); }); describe("POST /api/attachments", () => { it("uploads the validated File to Gate before persisting opaque metadata", async () => { const request = requestWithFile(); const authorization = request.headers.get("Authorization"); const response = await POST(request); expect(response.status).toBe(201); expect(await response.json()).toEqual({ attachment: { id: "00000000-0000-4000-8000-000000000077", name: "brief.txt", contentType: "text/plain", size: 5, }, }); expect(uploadGateFile).toHaveBeenCalledWith( expect.objectContaining({ name: "../brief.txt", size: 5, type: "text/plain" }), authorization, expect.any(AbortSignal), ); expect(createPendingAttachment).toHaveBeenCalledWith( expect.anything(), "00000000-0000-4000-8000-000000000001", { name: "brief.txt", contentType: "text/plain", size: 5, gateFileId: "file-opaque_123", }, ); expect(uploadGateFile.mock.invocationCallOrder[0]).toBeLessThan( createPendingAttachment.mock.invocationCallOrder[0], ); }); it("deletes the uploaded Gate file with the user token when DB persistence fails", async () => { createPendingAttachment.mockRejectedValue(new Error("private-db-sentinel")); const request = requestWithFile(); const authorization = request.headers.get("Authorization"); const response = await POST(request); expect(response.status).toBe(503); expect(JSON.stringify(await response.json())).not.toContain("private-db-sentinel"); expect(deleteGateFile).toHaveBeenCalledWith( "file-opaque_123", authorization, expect.any(AbortSignal), ); }); it("rejects unauthorized callers before uploading", async () => { authenticateRequest.mockRejectedValue(new AuthError()); const response = await POST(requestWithFile()); expect(response.status).toBe(401); expect(uploadGateFile).not.toHaveBeenCalled(); }); }); function requestWithFile(): Request { const form = new FormData(); form.append("file", new File(["brief"], "../brief.txt", { type: "text/plain" })); return new Request("http://localhost/api/attachments", { method: "POST", headers: { Authorization: "Bearer exact-user-jwt" }, body: form, }); }