| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 |
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
- import { MinioService } from '@/server/modules/files/minio.service';
- import { Client } from 'minio';
- import { logger } from '@/server/utils/logger';
- // Mock dependencies
- vi.mock('minio');
- vi.mock('@/server/utils/logger');
- // Mock process.env using vi.stubEnv for proper isolation
- beforeEach(() => {
- vi.stubEnv('MINIO_HOST', 'localhost');
- vi.stubEnv('MINIO_PORT', '9000');
- vi.stubEnv('MINIO_USE_SSL', 'false');
- vi.stubEnv('MINIO_ACCESS_KEY', 'minioadmin');
- vi.stubEnv('MINIO_SECRET_KEY', 'minioadmin');
- vi.stubEnv('MINIO_BUCKET_NAME', 'test-bucket');
- });
- afterEach(() => {
- vi.unstubAllEnvs();
- });
- describe('MinIO Integration Tests', () => {
- let minioService: MinioService;
- let mockClient: Client;
- beforeEach(() => {
- mockClient = new Client({} as any);
- (Client as any).mockClear();
- (Client as any).mockImplementation(() => mockClient);
- // Create MinioService with mock client
- minioService = new MinioService();
- });
- afterEach(() => {
- vi.clearAllMocks();
- });
- describe('Bucket Operations', () => {
- it('should ensure bucket exists and set policy', async () => {
- // Mock bucket doesn't exist
- mockClient.bucketExists = vi.fn().mockResolvedValue(false);
- mockClient.makeBucket = vi.fn().mockResolvedValue(undefined);
- mockClient.setBucketPolicy = vi.fn().mockResolvedValue(undefined);
- const result = await minioService.ensureBucketExists();
- expect(result).toBe(true);
- expect(mockClient.bucketExists).toHaveBeenCalledWith('test-bucket');
- expect(mockClient.makeBucket).toHaveBeenCalledWith('test-bucket');
- expect(mockClient.setBucketPolicy).toHaveBeenCalled();
- expect(logger.db).toHaveBeenCalledWith('Created new bucket: test-bucket');
- });
- it('should handle existing bucket', async () => {
- mockClient.bucketExists = vi.fn().mockResolvedValue(true);
- const result = await minioService.ensureBucketExists();
- expect(result).toBe(true);
- expect(mockClient.bucketExists).toHaveBeenCalledWith('test-bucket');
- expect(mockClient.makeBucket).not.toHaveBeenCalled();
- expect(mockClient.setBucketPolicy).not.toHaveBeenCalled();
- });
- });
- describe('File Operations', () => {
- it('should upload and download file successfully', async () => {
- const testContent = Buffer.from('Hello, MinIO!');
- const mockUrl = 'http://localhost:9000/test-bucket/test.txt';
- // Mock bucket operations
- mockClient.bucketExists = vi.fn().mockResolvedValue(true);
- mockClient.putObject = vi.fn().mockResolvedValue(undefined);
- mockClient.statObject = vi.fn().mockResolvedValue({ size: testContent.length } as any);
- mockClient.getObject = vi.fn().mockReturnValue({
- on: (event: string, callback: Function) => {
- if (event === 'data') callback(testContent);
- if (event === 'end') callback();
- }
- } as any);
- // Upload file
- const uploadUrl = await minioService.createObject('test-bucket', 'test.txt', testContent, 'text/plain');
- expect(uploadUrl).toBe(mockUrl);
- expect(mockClient.putObject).toHaveBeenCalledWith(
- 'test-bucket',
- 'test.txt',
- testContent,
- testContent.length,
- { 'Content-Type': 'text/plain' }
- );
- // Check file exists
- const exists = await minioService.objectExists('test-bucket', 'test.txt');
- expect(exists).toBe(true);
- expect(mockClient.statObject).toHaveBeenCalledWith('test-bucket', 'test.txt');
- });
- it('should handle file not found', async () => {
- const notFoundError = new Error('Object not found');
- notFoundError.message = 'not found';
- mockClient.statObject = vi.fn().mockRejectedValue(notFoundError);
- const exists = await minioService.objectExists('test-bucket', 'nonexistent.txt');
- expect(exists).toBe(false);
- });
- it('should delete file successfully', async () => {
- mockClient.removeObject = vi.fn().mockResolvedValue(undefined);
- await minioService.deleteObject('test-bucket', 'test.txt');
- expect(mockClient.removeObject).toHaveBeenCalledWith('test-bucket', 'test.txt');
- expect(logger.db).toHaveBeenCalledWith('Deleted object: test-bucket/test.txt');
- });
- });
- describe('Presigned URL Operations', () => {
- it('should generate presigned URLs correctly', async () => {
- const mockPresignedUrl = 'https://minio.example.com/presigned-url';
- mockClient.presignedGetObject = vi.fn().mockResolvedValue(mockPresignedUrl);
- // Test regular presigned URL
- const url = await minioService.getPresignedFileUrl('test-bucket', 'file.txt', 3600);
- expect(url).toBe(mockPresignedUrl);
- expect(mockClient.presignedGetObject).toHaveBeenCalledWith('test-bucket', 'file.txt', 3600);
- // Test download URL with content disposition
- const downloadUrl = await minioService.getPresignedFileDownloadUrl(
- 'test-bucket',
- 'file.txt',
- '测试文件.txt',
- 1800
- );
- expect(downloadUrl).toBe(mockPresignedUrl);
- expect(mockClient.presignedGetObject).toHaveBeenCalledWith(
- 'test-bucket',
- 'file.txt',
- 1800,
- {
- 'response-content-disposition': 'attachment; filename="%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.txt"',
- 'response-content-type': 'application/octet-stream'
- }
- );
- });
- });
- describe('Multipart Upload Operations', () => {
- it('should handle multipart upload workflow', async () => {
- const mockUploadId = 'upload-123';
- const mockPartUrls = ['url1', 'url2', 'url3'];
- const mockStat = { size: 3072 };
- // Mock multipart operations
- mockClient.initiateNewMultipartUpload = vi.fn().mockResolvedValue(mockUploadId);
- mockClient.presignedUrl = vi.fn()
- .mockResolvedValueOnce('url1')
- .mockResolvedValueOnce('url2')
- .mockResolvedValueOnce('url3');
- mockClient.completeMultipartUpload = vi.fn().mockResolvedValue(undefined);
- mockClient.statObject = vi.fn().mockResolvedValue(mockStat as any);
- // Create multipart upload
- const uploadId = await minioService.createMultipartUpload('test-bucket', 'large-file.zip');
- expect(uploadId).toBe(mockUploadId);
- expect(mockClient.initiateNewMultipartUpload).toHaveBeenCalledWith(
- 'test-bucket',
- 'large-file.zip',
- {}
- );
- // Generate part URLs
- const partUrls = await minioService.generateMultipartUploadUrls(
- 'test-bucket',
- 'large-file.zip',
- mockUploadId,
- 3
- );
- expect(partUrls).toEqual(mockPartUrls);
- expect(mockClient.presignedUrl).toHaveBeenCalledTimes(3);
- // Complete multipart upload
- const parts = [
- { ETag: 'etag1', PartNumber: 1 },
- { ETag: 'etag2', PartNumber: 2 },
- { ETag: 'etag3', PartNumber: 3 }
- ];
- const result = await minioService.completeMultipartUpload(
- 'test-bucket',
- 'large-file.zip',
- mockUploadId,
- parts
- );
- expect(result).toEqual({ size: 3072 });
- expect(mockClient.completeMultipartUpload).toHaveBeenCalledWith(
- 'test-bucket',
- 'large-file.zip',
- mockUploadId,
- [{ part: 1, etag: 'etag1' }, { part: 2, etag: 'etag2' }, { part: 3, etag: 'etag3' }]
- );
- });
- });
- describe('Error Handling', () => {
- it('should handle MinIO connection errors', async () => {
- const connectionError = new Error('Connection refused');
- mockClient.bucketExists = vi.fn().mockRejectedValue(connectionError);
- await expect(minioService.ensureBucketExists()).rejects.toThrow(connectionError);
- expect(logger.error).toHaveBeenCalledWith(
- 'Failed to ensure bucket exists: test-bucket',
- connectionError
- );
- });
- it('should handle file operation errors', async () => {
- const operationError = new Error('Operation failed');
- // 确保桶存在成功
- mockClient.bucketExists = vi.fn().mockResolvedValue(true);
- // 但文件操作失败
- mockClient.putObject = vi.fn().mockRejectedValue(operationError);
- await expect(minioService.createObject(
- 'test-bucket',
- 'test.txt',
- Buffer.from('test'),
- 'text/plain'
- )).rejects.toThrow(operationError);
- expect(logger.error).toHaveBeenCalledWith(
- 'Failed to create object test-bucket/test.txt:',
- operationError
- );
- });
- it('should handle permission errors gracefully', async () => {
- const permissionError = new Error('Permission denied');
- mockClient.statObject = vi.fn().mockRejectedValue(permissionError);
- await expect(minioService.objectExists('test-bucket', 'file.txt')).rejects.toThrow(permissionError);
- expect(logger.error).toHaveBeenCalledWith(
- 'Error checking existence of object test-bucket/file.txt:',
- permissionError
- );
- });
- });
- describe('Configuration Validation', () => {
- it('should validate MinIO configuration', () => {
- expect(minioService.bucketName).toBe('test-bucket');
- // Test URL generation with different configurations
- const url = minioService.getFileUrl('test-bucket', 'file.txt');
- expect(url).toBe('http://localhost:9000/test-bucket/file.txt');
- });
- it('should handle SSL configuration', async () => {
- // Create new instance with SSL
- vi.stubEnv('MINIO_USE_SSL', 'true');
- vi.stubEnv('MINIO_PORT', '443');
- const sslService = new MinioService();
- const url = sslService.getFileUrl('test-bucket', 'file.txt');
- expect(url).toBe('https://localhost:443/test-bucket/file.txt');
- });
- });
- describe('Performance Testing', () => {
- it('should handle concurrent operations', async () => {
- mockClient.presignedGetObject = vi.fn().mockResolvedValue('https://minio.example.com/file');
- // Test concurrent URL generation with smaller concurrency
- const promises = Array(5).fill(0).map((_, i) =>
- minioService.getPresignedFileUrl('test-bucket', `file${i}.txt`)
- );
- const results = await Promise.all(promises);
- expect(results).toHaveLength(5);
- expect(results.every(url => url === 'https://minio.example.com/file')).toBe(true);
- });
- it('should handle large file operations', async () => {
- // Use smaller buffer size to avoid memory issues
- const largeBuffer = Buffer.alloc(1 * 1024 * 1024); // 1MB instead of 10MB
- mockClient.bucketExists = vi.fn().mockResolvedValue(true);
- mockClient.putObject = vi.fn().mockResolvedValue({ etag: 'etag123', versionId: null });
- await minioService.createObject('test-bucket', 'large-file.bin', largeBuffer, 'application/octet-stream');
- expect(mockClient.putObject).toHaveBeenCalledWith(
- 'test-bucket',
- 'large-file.bin',
- largeBuffer,
- largeBuffer.length,
- { 'Content-Type': 'application/octet-stream' }
- );
- });
- });
- });
|