130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
adapterForProvider: vi.fn(),
|
|
deleteFile: vi.fn(async () => undefined),
|
|
downloadAttachment: vi.fn(),
|
|
ensureFreshMailboxCreds: vi.fn(),
|
|
fileCreate: vi.fn(async () => ({ id: "created" })),
|
|
linkCreate: vi.fn(async () => ({ id: "linked" })),
|
|
linkFindFirst: vi.fn(),
|
|
loadMailboxCreds: vi.fn(),
|
|
putBufferAsFile: vi.fn(async () => ({ publicUrl: null })),
|
|
withStorageReservation: vi.fn(),
|
|
events: [] as string[],
|
|
}));
|
|
|
|
const transaction = {
|
|
file: { create: mocks.fileCreate },
|
|
fileEntityLink: { create: mocks.linkCreate, findFirst: mocks.linkFindFirst },
|
|
};
|
|
|
|
vi.mock("@hivegrid/db/storage-capacity.server", () => ({
|
|
withTenantStorageWriteReservation: (...args: unknown[]) => mocks.withStorageReservation(...args),
|
|
}));
|
|
vi.mock("~/utils/db/.server/tenantRls.server", () => ({
|
|
withTenantRLS: async (_tenantId: string, operation: (tx: typeof transaction) => Promise<unknown>) => operation(transaction),
|
|
}));
|
|
vi.mock("~/utils/integrations/storageService", () => ({
|
|
deleteFile: (...args: unknown[]) => mocks.deleteFile(...args),
|
|
putBufferAsFile: async (...args: unknown[]) => {
|
|
mocks.events.push("upload");
|
|
return mocks.putBufferAsFile(...args);
|
|
},
|
|
resolveStorageBucket: () => "email-attachments",
|
|
}));
|
|
vi.mock("./MailboxConnectionService.server", () => ({
|
|
adapterForProvider: mocks.adapterForProvider,
|
|
ensureFreshMailboxCreds: mocks.ensureFreshMailboxCreds,
|
|
loadMailboxCreds: mocks.loadMailboxCreds,
|
|
}));
|
|
|
|
import { importAttachmentToFiles } from "./EmailAttachmentService.server";
|
|
|
|
describe("email attachment storage reservation", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mocks.events.length = 0;
|
|
mocks.linkFindFirst.mockResolvedValue(null);
|
|
mocks.loadMailboxCreds.mockResolvedValue({ accessToken: "encrypted" });
|
|
mocks.ensureFreshMailboxCreds.mockResolvedValue({ accessToken: "fresh" });
|
|
mocks.adapterForProvider.mockReturnValue({ downloadAttachment: mocks.downloadAttachment });
|
|
mocks.downloadAttachment.mockResolvedValue({
|
|
filename: "invoice.pdf",
|
|
mimeType: "application/pdf",
|
|
contentBase64: Buffer.from("hello").toString("base64"),
|
|
});
|
|
mocks.withStorageReservation.mockImplementation(async (input) => {
|
|
mocks.events.push("reserve");
|
|
const stored = await input.upload();
|
|
return input.runTransaction(async (tx) => {
|
|
mocks.events.push("commit");
|
|
return input.commit(tx, stored);
|
|
});
|
|
});
|
|
});
|
|
|
|
it("reserves the exact decoded byte size before object upload and commits File plus link", async () => {
|
|
await expect(
|
|
importAttachmentToFiles({
|
|
tenantId: "tenant_1",
|
|
userId: "user_1",
|
|
mailboxId: "mailbox_1",
|
|
provider: "microsoft",
|
|
messageId: "message_1",
|
|
providerMessageId: "provider-message",
|
|
attachmentId: "attachment_1",
|
|
})
|
|
).resolves.toEqual({ fileId: expect.stringMatching(/^file_/u) });
|
|
|
|
expect(mocks.events).toEqual(["reserve", "upload", "commit"]);
|
|
expect(mocks.withStorageReservation).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
reservation: expect.objectContaining({
|
|
tenantId: "tenant_1",
|
|
actorUserId: "user_1",
|
|
originalFilename: "invoice.pdf",
|
|
mimeType: "application/pdf",
|
|
sizeBytes: 5,
|
|
}),
|
|
})
|
|
);
|
|
expect(mocks.fileCreate).toHaveBeenCalledWith({
|
|
data: expect.objectContaining({ tenantId: "tenant_1", sizeBytes: 5, storageKey: expect.stringContaining("/original") }),
|
|
});
|
|
expect(mocks.linkCreate).toHaveBeenCalledWith({
|
|
data: expect.objectContaining({ entityType: "email_message", entityId: "message_1", linkRole: "attachment:attachment_1" }),
|
|
});
|
|
});
|
|
|
|
it("exposes an object rollback callback for failed database commits", async () => {
|
|
mocks.linkCreate.mockRejectedValueOnce(new Error("link failed"));
|
|
mocks.withStorageReservation.mockImplementationOnce(async (input) => {
|
|
const stored = await input.upload();
|
|
try {
|
|
return await input.runTransaction((tx) => input.commit(tx, stored));
|
|
} catch (error) {
|
|
await input.rollbackUpload(stored);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
await expect(
|
|
importAttachmentToFiles({
|
|
tenantId: "tenant_1",
|
|
userId: "user_1",
|
|
mailboxId: "mailbox_1",
|
|
provider: "microsoft",
|
|
messageId: "message_1",
|
|
providerMessageId: "provider-message",
|
|
attachmentId: "attachment_1",
|
|
})
|
|
).rejects.toThrow("link failed");
|
|
|
|
expect(mocks.deleteFile).toHaveBeenCalledWith({
|
|
bucket: "email-attachments",
|
|
key: expect.stringMatching(/^tenants\/tenant_1\/files\/file_.+\/original$/u),
|
|
});
|
|
});
|
|
});
|