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' } ); }); }); });