import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { DataSource } from 'typeorm'; import { FileService } from '@/modules/files/file.service'; import { File } from '@/modules/files/file.entity'; import { MinioService } from '@/modules/files/minio.service'; import { logger } from '@/utils/logger'; // Mock dependencies vi.mock('@/modules/files/minio.service'); vi.mock('@/utils/logger'); vi.mock('uuid', () => ({ v4: () => 'test-uuid-123' })); describe('FileService', () => { let mockDataSource: DataSource; beforeEach(() => { mockDataSource = { getRepository: vi.fn(() => ({ findOneBy: vi.fn(), save: vi.fn() })) } as unknown as DataSource; }); afterEach(() => { vi.clearAllMocks(); }); describe('createFile', () => { it('should create file with upload policy successfully', async () => { const mockFileData = { name: 'test.txt', type: 'text/plain', size: 1024, uploadUserId: 1 }; const mockUploadPolicy = { 'x-amz-algorithm': 'test-algorithm', 'x-amz-credential': 'test-credential', host: 'https://minio.example.com' }; const mockSavedFile = { id: 1, ...mockFileData, path: '1/test-uuid-123-test.txt', uploadTime: new Date(), createdAt: new Date(), updatedAt: new Date() }; const mockGenerateUploadPolicy = vi.fn().mockResolvedValue(mockUploadPolicy); vi.mocked(MinioService).mockImplementation(() => ({ generateUploadPolicy: mockGenerateUploadPolicy } as unknown as MinioService)); const fileService = new FileService(mockDataSource); // Mock GenericCrudService methods vi.spyOn(fileService, 'create').mockResolvedValue(mockSavedFile as File); const result = await fileService.createFile(mockFileData); expect(mockGenerateUploadPolicy).toHaveBeenCalledWith('1/test-uuid-123-test.txt'); expect(fileService.create).toHaveBeenCalledWith(expect.objectContaining({ name: 'test.txt', path: '1/test-uuid-123-test.txt', uploadUserId: 1 })); expect(result).toEqual({ file: mockSavedFile, uploadPolicy: mockUploadPolicy }); }); it('should handle errors during file creation', async () => { const mockFileData = { name: 'test.txt', uploadUserId: 1 }; const mockGenerateUploadPolicy = vi.fn().mockRejectedValue(new Error('MinIO error')); vi.mocked(MinioService).mockImplementation(() => ({ generateUploadPolicy: mockGenerateUploadPolicy } as unknown as MinioService)); const fileService = new FileService(mockDataSource); await expect(fileService.createFile(mockFileData)).rejects.toThrow('文件创建失败'); expect(logger.error).toHaveBeenCalled(); }); }); describe('deleteFile', () => { it('should delete file successfully when file exists', async () => { const mockFile = { id: 1, path: '1/test-file.txt', name: 'test-file.txt' } as File; const mockObjectExists = vi.fn().mockResolvedValue(true); const mockDeleteObject = vi.fn().mockResolvedValue(undefined); vi.mocked(MinioService).mockImplementation(() => ({ objectExists: mockObjectExists, deleteObject: mockDeleteObject, bucketName: 'd8dai' } as unknown as MinioService)); const fileService = new FileService(mockDataSource); vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile); vi.spyOn(fileService, 'delete').mockResolvedValue(true); const result = await fileService.deleteFile(1); expect(fileService.getById).toHaveBeenCalledWith(1); expect(mockObjectExists).toHaveBeenCalledWith('d8dai', '1/test-file.txt'); expect(mockDeleteObject).toHaveBeenCalledWith('d8dai', '1/test-file.txt'); expect(fileService.delete).toHaveBeenCalledWith(1); expect(result).toBe(true); }); it('should delete database record even when MinIO file not found', async () => { const mockFile = { id: 1, path: '1/test-file.txt', name: 'test-file.txt' } as File; const mockObjectExists = vi.fn().mockResolvedValue(false); vi.mocked(MinioService).mockImplementation(() => ({ objectExists: mockObjectExists, deleteObject: vi.fn(), bucketName: 'd8dai' } as unknown as MinioService)); const fileService = new FileService(mockDataSource); vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile); vi.spyOn(fileService, 'delete').mockResolvedValue(true); const result = await fileService.deleteFile(1); expect(mockObjectExists).toHaveBeenCalledWith('d8dai', '1/test-file.txt'); expect(fileService.delete).toHaveBeenCalledWith(1); expect(result).toBe(true); expect(logger.error).toHaveBeenCalled(); }); it('should throw error when file not found', async () => { const fileService = new FileService(mockDataSource); vi.spyOn(fileService, 'getById').mockResolvedValue(null); await expect(fileService.deleteFile(999)).rejects.toThrow('文件不存在'); }); }); describe('getFileUrl', () => { it('should return file URL successfully', async () => { const mockFile = { id: 1, path: '1/test-file.txt' } as File; const mockPresignedUrl = 'https://minio.example.com/presigned-url'; const mockGetPresignedFileUrl = vi.fn().mockResolvedValue(mockPresignedUrl); vi.mocked(MinioService).mockImplementation(() => ({ getPresignedFileUrl: mockGetPresignedFileUrl, bucketName: 'd8dai' } as unknown as MinioService)); const fileService = new FileService(mockDataSource); vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile); const result = await fileService.getFileUrl(1); expect(fileService.getById).toHaveBeenCalledWith(1); expect(mockGetPresignedFileUrl).toHaveBeenCalledWith('d8dai', '1/test-file.txt'); expect(result).toBe(mockPresignedUrl); }); it('should throw error when file not found', async () => { const fileService = new FileService(mockDataSource); vi.spyOn(fileService, 'getById').mockResolvedValue(null); await expect(fileService.getFileUrl(999)).rejects.toThrow('文件不存在'); }); }); describe('getFileDownloadUrl', () => { it('should return download URL with filename', async () => { const mockFile = { id: 1, path: '1/test-file.txt', name: '测试文件.txt' } as File; const mockPresignedUrl = 'https://minio.example.com/download-url'; const mockGetPresignedFileDownloadUrl = vi.fn().mockResolvedValue(mockPresignedUrl); vi.mocked(MinioService).mockImplementation(() => ({ getPresignedFileDownloadUrl: mockGetPresignedFileDownloadUrl, bucketName: 'd8dai' } as unknown as MinioService)); const fileService = new FileService(mockDataSource); vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile); const result = await fileService.getFileDownloadUrl(1); expect(fileService.getById).toHaveBeenCalledWith(1); expect(mockGetPresignedFileDownloadUrl).toHaveBeenCalledWith( 'd8dai', '1/test-file.txt', '测试文件.txt' ); expect(result).toEqual({ url: mockPresignedUrl, filename: '测试文件.txt' }); }); it('should throw error when file not found', async () => { const fileService = new FileService(mockDataSource); vi.spyOn(fileService, 'getById').mockResolvedValue(null); await expect(fileService.getFileDownloadUrl(999)).rejects.toThrow('文件不存在'); }); }); describe('createMultipartUploadPolicy', () => { it('should create multipart upload policy successfully', async () => { const mockFileData = { name: 'large-file.zip', type: 'application/zip', uploadUserId: 1 }; const mockUploadId = 'upload-123'; const mockUploadUrls = ['url1', 'url2', 'url3']; const mockSavedFile = { id: 1, ...mockFileData, path: '1/test-uuid-123-large-file.zip', uploadTime: new Date(), createdAt: new Date(), updatedAt: new Date() } as File; const mockCreateMultipartUpload = vi.fn().mockResolvedValue(mockUploadId); const mockGenerateMultipartUploadUrls = vi.fn().mockResolvedValue(mockUploadUrls); vi.mocked(MinioService).mockImplementation(() => ({ createMultipartUpload: mockCreateMultipartUpload, generateMultipartUploadUrls: mockGenerateMultipartUploadUrls, bucketName: 'd8dai' } as unknown as MinioService)); const fileService = new FileService(mockDataSource); vi.spyOn(fileService, 'create').mockResolvedValue(mockSavedFile); const result = await fileService.createMultipartUploadPolicy(mockFileData, 3); expect(mockCreateMultipartUpload).toHaveBeenCalledWith('d8dai', '1/test-uuid-123-large-file.zip'); expect(mockGenerateMultipartUploadUrls).toHaveBeenCalledWith( 'd8dai', '1/test-uuid-123-large-file.zip', mockUploadId, 3 ); expect(result).toEqual({ file: mockSavedFile, uploadId: mockUploadId, uploadUrls: mockUploadUrls, bucket: 'd8dai', key: '1/test-uuid-123-large-file.zip' }); }); it('should handle errors during multipart upload creation', async () => { const mockFileData = { name: 'large-file.zip', uploadUserId: 1 }; const mockCreateMultipartUpload = vi.fn().mockRejectedValue(new Error('MinIO error')); vi.mocked(MinioService).mockImplementation(() => ({ createMultipartUpload: mockCreateMultipartUpload, bucketName: 'd8dai' } as unknown as MinioService)); const fileService = new FileService(mockDataSource); await expect(fileService.createMultipartUploadPolicy(mockFileData, 3)).rejects.toThrow('创建多部分上传策略失败'); expect(logger.error).toHaveBeenCalled(); }); }); describe('completeMultipartUpload', () => { it('should complete multipart upload successfully', async () => { const uploadData = { uploadId: 'upload-123', bucket: 'd8dai', key: '1/test-file.txt', parts: [ { partNumber: 1, etag: 'etag1' }, { partNumber: 2, etag: 'etag2' } ] }; const mockFile = { id: 1, path: '1/test-file.txt', size: 0, updatedAt: new Date() } as File; const mockCompleteResult = { size: 2048 }; const mockFileUrl = 'https://minio.example.com/file.txt'; const mockCompleteMultipartUpload = vi.fn().mockResolvedValue(mockCompleteResult); const mockGetFileUrl = vi.fn().mockReturnValue(mockFileUrl); vi.mocked(MinioService).mockImplementation(() => ({ completeMultipartUpload: mockCompleteMultipartUpload, getFileUrl: mockGetFileUrl } as unknown as MinioService)); const mockRepository = { findOneBy: vi.fn().mockResolvedValue(mockFile), save: vi.fn().mockResolvedValue({ ...mockFile, size: 2048 } as File) }; mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository); const fileService = new FileService(mockDataSource); const result = await fileService.completeMultipartUpload(uploadData); expect(mockCompleteMultipartUpload).toHaveBeenCalledWith( 'd8dai', '1/test-file.txt', 'upload-123', [{ PartNumber: 1, ETag: 'etag1' }, { PartNumber: 2, ETag: 'etag2' }] ); expect(mockRepository.findOneBy).toHaveBeenCalledWith({ path: '1/test-file.txt' }); expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining({ size: 2048 })); expect(result).toEqual({ fileId: 1, url: mockFileUrl, key: '1/test-file.txt', size: 2048 }); }); it('should throw error when file record not found', async () => { const uploadData = { uploadId: 'upload-123', bucket: 'd8dai', key: '1/nonexistent.txt', parts: [{ partNumber: 1, etag: 'etag1' }] }; const mockCompleteMultipartUpload = vi.fn().mockResolvedValue({ size: 1024 }); vi.mocked(MinioService).mockImplementation(() => ({ completeMultipartUpload: mockCompleteMultipartUpload } as unknown as MinioService)); const mockRepository = { findOneBy: vi.fn().mockResolvedValue(null) }; mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository); const fileService = new FileService(mockDataSource); await expect(fileService.completeMultipartUpload(uploadData)).rejects.toThrow('文件记录不存在'); }); it('should handle errors during completion', async () => { const uploadData = { uploadId: 'upload-123', bucket: 'd8dai', key: '1/test-file.txt', parts: [{ partNumber: 1, etag: 'etag1' }] }; const mockFile = { id: 1, path: '1/test-file.txt', size: 0, updatedAt: new Date() } as File; const mockRepository = { findOneBy: vi.fn().mockResolvedValue(mockFile), save: vi.fn() }; const mockCompleteMultipartUpload = vi.fn().mockRejectedValue(new Error('Completion failed')); mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository); vi.mocked(MinioService).mockImplementation(() => ({ completeMultipartUpload: mockCompleteMultipartUpload } as unknown as MinioService)); const fileService = new FileService(mockDataSource); await expect(fileService.completeMultipartUpload(uploadData)).rejects.toThrow('完成分片上传失败'); expect(logger.error).toHaveBeenCalled(); }); }); });