import { S3StorageAdapter, S3StorageConfig, } from '@/lib/storage/storage.adapter'; import { DeleteObjectCommand, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3'; import * as presigner from '@aws-sdk/s3-request-presigner'; import { mockClient } from 'aws-sdk-client-mock'; jest.mock('@aws-sdk/s3-request-presigner'); describe('S3StorageAdapter', () => { let s3Mock: ReturnType; let mockS3Client: S3Client; let adapter: S3StorageAdapter; const config: S3StorageConfig = { endpoint: 'http://localhost:9000', bucket: 'test-bucket', region: 'us-east-1', accessKey: 'test-access-key', secretKey: 'test-secret-key', publicUrl: 'http://test.com', }; beforeEach(() => { s3Mock = mockClient(S3Client); mockS3Client = new S3Client({ region: 'us-east-1' }); adapter = new S3StorageAdapter(config, mockS3Client); }); afterEach(() => { s3Mock.restore(); jest.clearAllMocks(); }); describe('get', () => { it('should return public URL for key', async () => { const key = 'test-image.jpg'; const result = await adapter.get(key); expect(result).toEqual({ ok: true, value: `http://test.com/${key}`, }); }); it('should handle nested keys', async () => { const key = 'articles/2026/04/image.jpg'; const result = await adapter.get(key); expect(result).toEqual({ ok: true, value: `http://test.com/${key}`, }); }); }); describe('put', () => { it('should call presigner with correct command parameters', async () => { const key = 'test-image.jpg'; jest.mocked(presigner.getSignedUrl).mockResolvedValue( 'https://presigned-url.example.com' ); await adapter.put(key, 'image/jpeg'); const [, command] = jest.mocked(presigner.getSignedUrl).mock .calls[0]; expect(command).toBeInstanceOf(PutObjectCommand); expect((command as PutObjectCommand).input.Bucket).toBe( 'test-bucket' ); expect((command as PutObjectCommand).input.Key).toBe(key); expect((command as PutObjectCommand).input.ContentType).toBe( 'image/jpeg' ); }); it('should use 3600 second expiry', async () => { jest.mocked(presigner.getSignedUrl).mockResolvedValue( 'https://presigned-url.example.com' ); await adapter.put('test-image.jpg', 'image/jpeg'); const [, , options] = jest.mocked(presigner.getSignedUrl).mock .calls[0]; expect(options).toEqual({ expiresIn: 3600 }); }); it('should return ok result with the presigned URL', async () => { const presignedUrl = 'https://presigned-url.example.com'; jest.mocked(presigner.getSignedUrl).mockResolvedValue(presignedUrl); const result = await adapter.put('test-image.jpg', 'image/jpeg'); expect(result).toEqual({ ok: true, value: presignedUrl }); }); it('should return correct presigned URL for different content types', async () => { const presignedUrl = 'https://presigned-url.example.com/my-image.png'; jest.mocked(presigner.getSignedUrl).mockResolvedValue(presignedUrl); const result = await adapter.put('my-image.png', 'image/png'); expect(result).toEqual({ ok: true, value: presignedUrl }); }); it('should return error result on presigner failure', async () => { jest.mocked(presigner.getSignedUrl).mockRejectedValue( new Error('Presigner error') ); const result = await adapter.put('test-image.jpg', 'image/jpeg'); expect(result.ok).toBe(false); }); }); describe('delete', () => { it('should delete object from S3 with correct parameters', async () => { const key = 'test-image.jpg'; s3Mock.on(DeleteObjectCommand).resolves({}); await adapter.delete(key); expect(s3Mock.call(0).args[0]).toBeInstanceOf(DeleteObjectCommand); const command = s3Mock.call(0).args[0] as DeleteObjectCommand; expect(command.input.Bucket).toBe('test-bucket'); expect(command.input.Key).toBe(key); }); it('should return ok result on success', async () => { s3Mock.on(DeleteObjectCommand).resolves({}); const result = await adapter.delete('test-image.jpg'); expect(result).toEqual({ ok: true, value: undefined }); }); it('should return error result on failure', async () => { s3Mock.on(DeleteObjectCommand).rejects(new Error('S3 error')); const result = await adapter.delete('test-image.jpg'); expect(result.ok).toBe(false); }); it('should handle nested keys', async () => { const key = 'articles/2026/04/image.jpg'; s3Mock.on(DeleteObjectCommand).resolves({}); await adapter.delete(key); const command = s3Mock.call(0).args[0] as DeleteObjectCommand; expect(command.input.Key).toBe(key); }); }); });