80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
deleteFile: vi.fn(),
|
|
findFirst: vi.fn(),
|
|
deleteRow: vi.fn(),
|
|
publishSearchProjectionDelete: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("./storageService", () => ({
|
|
assembleChunkedUpload: vi.fn(),
|
|
deleteFile: mocks.deleteFile,
|
|
putBufferAsFile: vi.fn(),
|
|
resolveStorageBucket: () => "tenant-files",
|
|
storeFile: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@hivegrid/storage", () => ({
|
|
buildFileStorageKeys: vi.fn(),
|
|
buildUploadChunkKey: vi.fn(),
|
|
uploadChunkPrefix: vi.fn(),
|
|
createMagicBytesScanAdapter: () => ({ scan: vi.fn() }),
|
|
DEFAULT_ALLOW_LIST: { formUpload: [] },
|
|
}));
|
|
|
|
vi.mock("~/utils/db/.server/tenantRls.server", () => ({
|
|
withTenantRLS: async (_tenantId: string, callback: (tx: unknown) => Promise<unknown>) =>
|
|
callback({ file: { findFirst: mocks.findFirst, delete: mocks.deleteRow } }),
|
|
}));
|
|
|
|
vi.mock("~/services/search/searchIndexProjection.server", () => ({
|
|
publishFileSearchUpsert: vi.fn(),
|
|
publishSearchProjectionDelete: mocks.publishSearchProjectionDelete,
|
|
}));
|
|
|
|
import { hardDeleteFile } from "./fileService.server";
|
|
|
|
describe("hardDeleteFile", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mocks.findFirst.mockResolvedValue({
|
|
id: "file_1",
|
|
storageKey: "tenants/tenant_1/files/file_1/original.pdf",
|
|
previewStorageKey: "tenants/tenant_1/files/file_1/preview.png",
|
|
thumbnailStorageKey: "tenants/tenant_1/files/file_1/thumbnail.png",
|
|
});
|
|
mocks.deleteFile.mockResolvedValue(undefined);
|
|
mocks.deleteRow.mockResolvedValue({ id: "file_1" });
|
|
mocks.publishSearchProjectionDelete.mockResolvedValue(undefined);
|
|
});
|
|
|
|
it("removes every stored artifact before releasing the database quota", async () => {
|
|
await expect(hardDeleteFile({ tenantId: "tenant_1", fileId: "file_1" })).resolves.toBeUndefined();
|
|
|
|
expect(mocks.deleteFile).toHaveBeenNthCalledWith(1, {
|
|
bucket: "tenant-files",
|
|
key: "tenants/tenant_1/files/file_1/original.pdf",
|
|
});
|
|
expect(mocks.deleteFile).toHaveBeenNthCalledWith(2, {
|
|
bucket: "tenant-files",
|
|
key: "tenants/tenant_1/files/file_1/preview.png",
|
|
});
|
|
expect(mocks.deleteFile).toHaveBeenNthCalledWith(3, {
|
|
bucket: "tenant-files",
|
|
key: "tenants/tenant_1/files/file_1/thumbnail.png",
|
|
});
|
|
expect(mocks.deleteRow).toHaveBeenCalledWith({ where: { id: "file_1" } });
|
|
expect(mocks.deleteFile.mock.invocationCallOrder[2]).toBeLessThan(mocks.deleteRow.mock.invocationCallOrder[0]!);
|
|
});
|
|
|
|
it("keeps the database row when object deletion fails so accounting cannot lose the object", async () => {
|
|
mocks.deleteFile.mockRejectedValueOnce(new Error("object storage unavailable"));
|
|
|
|
await expect(hardDeleteFile({ tenantId: "tenant_1", fileId: "file_1" })).rejects.toThrow("object storage unavailable");
|
|
|
|
expect(mocks.deleteRow).not.toHaveBeenCalled();
|
|
expect(mocks.publishSearchProjectionDelete).not.toHaveBeenCalled();
|
|
});
|
|
});
|