import { beforeEach, describe, expect, mock, test } from "bun:test"; // Mock the S3 client send method const mockSend = mock(() => Promise.resolve({})); const mockGetSignedUrl = mock(() => Promise.resolve("https://s3.example.com/gearbox-images/test.jpg?signed=1"), ); // Mock modules before importing the service mock.module("@aws-sdk/client-s3", () => ({ S3Client: class MockS3Client { send = mockSend; }, PutObjectCommand: class MockPutObjectCommand { input: Record; constructor(input: Record) { this.input = input; } }, DeleteObjectCommand: class MockDeleteObjectCommand { input: Record; constructor(input: Record) { this.input = input; } }, GetObjectCommand: class MockGetObjectCommand { input: Record; constructor(input: Record) { this.input = input; } }, })); mock.module("@aws-sdk/s3-request-presigner", () => ({ getSignedUrl: mockGetSignedUrl, })); // Set env vars before importing the service process.env.S3_ENDPOINT = "http://localhost:3900"; process.env.S3_ACCESS_KEY = "test-access-key"; process.env.S3_SECRET_KEY = "test-secret-key"; process.env.S3_BUCKET = "gearbox-images"; process.env.S3_REGION = "garage"; // Import after mocking const { uploadImage, deleteImage, getImageUrl, withImageUrl, withImageUrls } = await import("@/server/services/storage.service"); describe("storage.service", () => { beforeEach(() => { mockSend.mockClear(); mockGetSignedUrl.mockClear(); mockGetSignedUrl.mockResolvedValue( "https://s3.example.com/gearbox-images/test.jpg?signed=1", ); }); describe("uploadImage", () => { test("calls PutObjectCommand with correct params", async () => { const buffer = Buffer.from("fake-image-data"); await uploadImage(buffer, "test-image.jpg", "image/jpeg"); expect(mockSend).toHaveBeenCalledTimes(1); const command = mockSend.mock.calls[0][0] as { input: Record; }; expect(command.input.Bucket).toBe("gearbox-images"); expect(command.input.Key).toBe("test-image.jpg"); expect(command.input.ContentType).toBe("image/jpeg"); expect(Buffer.isBuffer(command.input.Body)).toBe(true); }); test("handles ArrayBuffer input", async () => { const arrayBuffer = new ArrayBuffer(10); await uploadImage(arrayBuffer, "test.png", "image/png"); expect(mockSend).toHaveBeenCalledTimes(1); const command = mockSend.mock.calls[0][0] as { input: Record; }; expect(Buffer.isBuffer(command.input.Body)).toBe(true); }); }); describe("deleteImage", () => { test("calls DeleteObjectCommand with correct params", async () => { await deleteImage("test-image.jpg"); expect(mockSend).toHaveBeenCalledTimes(1); const command = mockSend.mock.calls[0][0] as { input: Record; }; expect(command.input.Bucket).toBe("gearbox-images"); expect(command.input.Key).toBe("test-image.jpg"); }); }); describe("getImageUrl", () => { test("calls getSignedUrl and returns result", async () => { const url = await getImageUrl("test-image.jpg"); expect(mockGetSignedUrl).toHaveBeenCalledTimes(1); expect(url).toBe( "https://s3.example.com/gearbox-images/test.jpg?signed=1", ); }); }); describe("withImageUrl", () => { test("returns null imageUrl when imageFilename is null", async () => { const record = { id: 1, name: "Test Item", imageFilename: null }; const result = await withImageUrl(record); expect(result.imageUrl).toBeNull(); expect(result.id).toBe(1); expect(result.name).toBe("Test Item"); expect(mockGetSignedUrl).not.toHaveBeenCalled(); }); test("returns presigned URL when imageFilename is present", async () => { const record = { id: 1, name: "Test Item", imageFilename: "photo.jpg", }; const result = await withImageUrl(record); expect(result.imageUrl).toBe( "https://s3.example.com/gearbox-images/test.jpg?signed=1", ); expect(result.id).toBe(1); expect(mockGetSignedUrl).toHaveBeenCalledTimes(1); }); }); describe("withImageUrls", () => { test("processes array of records correctly", async () => { const records = [ { id: 1, imageFilename: "a.jpg" }, { id: 2, imageFilename: null }, { id: 3, imageFilename: "b.png" }, ]; const results = await withImageUrls(records); expect(results).toHaveLength(3); expect(results[0].imageUrl).toBe( "https://s3.example.com/gearbox-images/test.jpg?signed=1", ); expect(results[1].imageUrl).toBeNull(); expect(results[2].imageUrl).toBe( "https://s3.example.com/gearbox-images/test.jpg?signed=1", ); // Called twice: for records[0] and records[2] expect(mockGetSignedUrl).toHaveBeenCalledTimes(2); }); test("handles empty array", async () => { const results = await withImageUrls([]); expect(results).toHaveLength(0); }); }); });