Files
hideyoshi-blog/tests/lib/storage/storage.s3.test.ts
Vitor Hideyoshi e2960027f2
All checks were successful
Build and Test / run-test (20.x) (push) Successful in 2m5s
refactor: update article service methods to use external ID and improve caching
2026-04-17 01:43:31 -03:00

167 lines
5.4 KiB
TypeScript

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<typeof mockClient>;
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);
});
});
});