From fc9d96f24204bfdb4a995f2e1c8eb57dea5d032f Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Sat, 11 Apr 2026 01:24:11 -0300 Subject: [PATCH] feat: enhance S3StorageAdapter tests with presigner integration and improved key handling --- tests/lib/storage/storage.s3.test.ts | 176 +++++++++++++++------------ 1 file changed, 99 insertions(+), 77 deletions(-) diff --git a/tests/lib/storage/storage.s3.test.ts b/tests/lib/storage/storage.s3.test.ts index 206f874..fbba29d 100644 --- a/tests/lib/storage/storage.s3.test.ts +++ b/tests/lib/storage/storage.s3.test.ts @@ -1,113 +1,131 @@ -import { S3StorageAdapter } from '@/lib/storage/storage.adapter'; +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', + }; + beforeEach(() => { s3Mock = mockClient(S3Client); mockS3Client = new S3Client({ region: 'us-east-1' }); - adapter = new S3StorageAdapter( - 'test-bucket', - 'us-east-1', - mockS3Client - ); + adapter = new S3StorageAdapter(config, mockS3Client); }); afterEach(() => { s3Mock.restore(); + jest.clearAllMocks(); }); - describe('put', () => { - it('should upload file to S3 with correct parameters', async () => { + describe('get', () => { + it('should return public URL for key', async () => { const key = 'test-image.jpg'; - const fileContent = 'test file content'; - const file = Buffer.from(fileContent); - s3Mock.on(PutObjectCommand).resolves({}); - - await adapter.put(key, file, 'image/jpeg'); - - expect(s3Mock.call(0).args[0]).toBeInstanceOf(PutObjectCommand); - const command = s3Mock.call(0).args[0] as PutObjectCommand; - expect(command.input.Bucket).toBe('test-bucket'); - expect(command.input.Key).toBe(key); - expect(command.input.ContentType).toBe('image/jpeg'); - }); - - it('should return StorageResult with s3 type', async () => { - const key = 'test-image.jpg'; - const file = Buffer.from('test file content'); - - s3Mock.on(PutObjectCommand).resolves({}); - - const result = await adapter.put(key, file, 'image/jpeg'); + const result = await adapter.get(key); expect(result).toEqual({ - type: 's3', - provider: 'aws-s3', - key, - publicUrl: `https://test-bucket.s3.us-east-1.amazonaws.com/${key}`, + ok: true, + value: `http://localhost:9000/test-bucket/${key}`, }); }); - it('should return correct public URL with different regions', async () => { - const adapter2 = new S3StorageAdapter( - 'my-bucket', - 'eu-west-1', - mockS3Client - ); - const key = 'my-image.png'; - const file = Buffer.from('test'); - - s3Mock.on(PutObjectCommand).resolves({}); - - const result = await adapter2.put(key, file, 'image/png'); - - expect(result.publicUrl).toBe( - `https://my-bucket.s3.eu-west-1.amazonaws.com/${key}` - ); - }); - - it('should handle Blob input', async () => { - const key = 'test-blob.jpg'; - const fileContent = 'blob content'; - const blob = new Blob([fileContent], { type: 'image/jpeg' }); - - s3Mock.on(PutObjectCommand).resolves({}); - - const result = await adapter.put(key, blob, 'image/jpeg'); - - expect(result.type).toBe('s3'); - expect(result.key).toBe(key); - }); - it('should handle nested keys', async () => { const key = 'articles/2026/04/image.jpg'; - const file = Buffer.from('test'); - s3Mock.on(PutObjectCommand).resolves({}); + const result = await adapter.get(key); - const result = await adapter.put(key, file, 'image/jpeg'); + expect(result).toEqual({ + ok: true, + value: `http://localhost:9000/test-bucket/${key}`, + }); + }); + }); - expect(result.key).toBe(key); - expect(result.publicUrl).toBe( - `https://test-bucket.s3.us-east-1.amazonaws.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); @@ -118,20 +136,24 @@ describe('S3StorageAdapter', () => { expect(command.input.Key).toBe(key); }); - it('should not throw error if deletion fails', async () => { - const key = 'test-image.jpg'; + 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')); - // Should not throw - expect(async () => { - await adapter.delete(key); - }).not.toThrow(); + 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);