mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 09:26:42 +02:00
feat(storage): improve shared-storage robustness, scalability, and DX
- Fix exists() to only catch 404/NotFound, rethrow real errors - Add downloadStream() for memory-efficient large file downloads - Add uploadMultipart() using @aws-sdk/lib-storage for large files - Add automatic pagination to list() via continuation tokens - Add CDN URL support (cdnUrl in BucketConfig, getCdnUrl() method) - Reduce factory boilerplate with generic createStorage() function - Add MinIO lifecycle rules for tmp/ prefixes (chat 90d, calendar 30d, picture 7d) - Add vitest setup with 56 tests covering client, factory, and utils Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ab42c265e1
commit
41fbd2f035
12 changed files with 1450 additions and 783 deletions
132
packages/shared-storage/src/factory.spec.ts
Normal file
132
packages/shared-storage/src/factory.spec.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// Mock the client module — StorageClient must be a class
|
||||
vi.mock('./client', () => ({
|
||||
StorageClient: vi.fn(function (this: Record<string, unknown>) {
|
||||
this.upload = vi.fn();
|
||||
this.getBucketName = vi.fn();
|
||||
}),
|
||||
}));
|
||||
|
||||
import {
|
||||
createStorage,
|
||||
createStorageClient,
|
||||
createPictureStorage,
|
||||
createChatStorage,
|
||||
} from './factory';
|
||||
import { BUCKETS } from './types';
|
||||
import { StorageClient } from './client';
|
||||
|
||||
describe('createStorage', () => {
|
||||
beforeEach(() => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.PICTURE_STORAGE_PUBLIC_URL;
|
||||
delete process.env.PICTURE_CDN_URL;
|
||||
});
|
||||
|
||||
it('creates a client with the correct bucket name', () => {
|
||||
createStorage('PICTURE');
|
||||
|
||||
expect(StorageClient).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ name: BUCKETS.PICTURE })
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves public URL from environment', () => {
|
||||
process.env.PICTURE_STORAGE_PUBLIC_URL = 'https://cdn.example.com/picture';
|
||||
|
||||
createStorage('PICTURE');
|
||||
|
||||
expect(StorageClient).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ publicUrl: 'https://cdn.example.com/picture' })
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves CDN URL from environment', () => {
|
||||
process.env.PICTURE_CDN_URL = 'https://cdn.fast.com/picture';
|
||||
|
||||
createStorage('PICTURE');
|
||||
|
||||
expect(StorageClient).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ cdnUrl: 'https://cdn.fast.com/picture' })
|
||||
);
|
||||
});
|
||||
|
||||
it('allows explicit publicUrl override', () => {
|
||||
process.env.PICTURE_STORAGE_PUBLIC_URL = 'https://from-env.com';
|
||||
|
||||
createStorage('PICTURE', 'https://explicit.com');
|
||||
|
||||
expect(StorageClient).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ publicUrl: 'https://explicit.com' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convenience aliases', () => {
|
||||
it('createPictureStorage creates PICTURE bucket', () => {
|
||||
createPictureStorage();
|
||||
|
||||
expect(StorageClient).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ name: BUCKETS.PICTURE })
|
||||
);
|
||||
});
|
||||
|
||||
it('createChatStorage creates CHAT bucket', () => {
|
||||
createChatStorage();
|
||||
|
||||
expect(StorageClient).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ name: BUCKETS.CHAT })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createStorageClient', () => {
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
delete process.env.S3_ENDPOINT;
|
||||
delete process.env.S3_ACCESS_KEY;
|
||||
delete process.env.S3_SECRET_KEY;
|
||||
});
|
||||
|
||||
it('throws when endpoint is missing in production', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.S3_ENDPOINT = '';
|
||||
|
||||
expect(() => createStorageClient(BUCKETS.CHAT)).toThrow('S3_ENDPOINT is required');
|
||||
});
|
||||
|
||||
it('throws when credentials are missing in production', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.S3_ENDPOINT = 'https://s3.example.com';
|
||||
process.env.S3_ACCESS_KEY = '';
|
||||
process.env.S3_SECRET_KEY = '';
|
||||
|
||||
expect(() => createStorageClient(BUCKETS.CHAT)).toThrow(
|
||||
'S3_ACCESS_KEY and S3_SECRET_KEY are required'
|
||||
);
|
||||
});
|
||||
|
||||
it('uses MinIO defaults in development', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
createStorageClient(BUCKETS.CHAT);
|
||||
|
||||
expect(StorageClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
endpoint: 'http://localhost:9000',
|
||||
accessKeyId: 'minioadmin',
|
||||
}),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue