// @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<typeof import("@/lib/auth/server")>()),
authenticateRequest,
}));
vi.mock("@/lib/db/attachments", () => ({ createPendingAttachment }));
vi.mock("@/lib/chat/gate-files", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/chat/gate-files")>()),
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,
});
}