import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { DataSource } from 'typeorm'; import { FileService } from '../file.service'; import { File } from '../file.entity'; import { MinioService } from '../minio.service'; import { logger } from '@/server/utils/logger'; // Mock dependencies vi.mock('../minio.service'); vi.mock('@/server/utils/logger'); vi.mock('uuid', () => ({ v4: () => 'test-uuid-123' })); describe('FileService', () => { let mockDataSource: DataSource; beforeEach(() => { mockDataSource = { getRepository: 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'; // vi.mocked(mockMinioService.completeMultipartUpload).mockResolvedValue(mockCompleteResult); // vi.mocked(mockMinioService.getFileUrl).mockReturnValue(mockFileUrl); // vi.spyOn(fileService.repository, 'findOneBy').mockResolvedValue(mockFile); // vi.spyOn(fileService.repository, 'save').mockResolvedValue(mockFile); // const result = await fileService.completeMultipartUpload(uploadData); // expect(mockMinioService.completeMultipartUpload).toHaveBeenCalledWith( // 'd8dai', // '1/test-file.txt', // 'upload-123', // [{ PartNumber: 1, ETag: 'etag1' }, { PartNumber: 2, ETag: 'etag2' }] // ); // expect(fileService.repository.findOneBy).toHaveBeenCalledWith({ path: '1/test-file.txt' }); // expect(fileService.repository.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' }] // }; // vi.mocked(mockMinioService.completeMultipartUpload).mockResolvedValue({ size: 1024 }); // vi.spyOn(fileService.repository, 'findOneBy').mockResolvedValue(null); // 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' }] // }; // vi.mocked(mockMinioService.completeMultipartUpload).mockRejectedValue(new Error('Completion failed')); // await expect(fileService.completeMultipartUpload(uploadData)).rejects.toThrow('完成分片上传失败'); // expect(logger.error).toHaveBeenCalled(); // }); // }); });