import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { testClient } from 'hono/testing'; import { DataSource } from 'typeorm'; import { File } from '@/server/modules/files/file.entity'; import { FileService } from '@/server/modules/files/file.service'; import { MinioService } from '@/server/modules/files/minio.service'; import { authMiddleware } from '@/server/middleware/auth.middleware'; import { fileApiRoutes } from '@/server/api'; // Mock dependencies vi.mock('@/server/modules/files/file.service', () => ({ FileService: vi.fn().mockImplementation(() => ({ createFile: vi.fn() })) })); vi.mock('@/server/modules/files/minio.service'); vi.mock('@/server/middleware/auth.middleware'); describe('File API Integration Tests', () => { let client: ReturnType>['api']['v1']; let mockFileService: FileService; let mockDataSource: DataSource; beforeEach(async () => { vi.clearAllMocks(); mockDataSource = {} as DataSource; // Mock auth middleware to bypass authentication vi.mocked(authMiddleware).mockImplementation(async (_, next) => { await next(); }); // Get the mocked FileService instance mockFileService = new FileService(mockDataSource); client = testClient(fileApiRoutes).api.v1; }); afterEach(() => { vi.clearAllMocks(); }); describe('POST /api/v1/files/upload-policy', () => { it('should generate upload policy successfully', async () => { const mockFileData = { name: 'test.txt', type: 'text/plain', size: 1024, path: '/uploads/test.txt', description: 'Test file', uploadUserId: 1 }; const mockResponse = { file: { id: 1, ...mockFileData, path: '1/test-uuid-123-test.txt', uploadTime: new Date(), createdAt: new Date(), updatedAt: new Date() }, uploadPolicy: { host: 'https://minio.example.com', key: '1/test-uuid-123-test.txt', bucket: 'd8dai' } }; mockFileService.createFile = vi.fn().mockResolvedValue(mockResponse); const response = await client.files['upload-policy'].$post({ json: mockFileData }, { headers: { 'Authorization': 'Bearer test-token' } }); if (response.status !== 200) { const error = await response.json(); console.debug('Error response:', JSON.stringify(error, null, 2)); console.debug('Response status:', response.status); } expect(response.status).toBe(200); const result = await response.json(); expect(result).toEqual(mockResponse); expect(mockFileService.createFile).toHaveBeenCalledWith(mockFileData); }); it('should return 400 for invalid request data', async () => { const invalidData = { name: '', // Empty name type: 'text/plain' }; const response = await client.files['upload-policy'].$post({ json: invalidData }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(400); }); it('should handle service errors gracefully', async () => { const mockFileData = { name: 'test.txt', type: 'text/plain', uploadUserId: 1 }; mockFileService.createFile = vi.fn().mockRejectedValue(new Error('Service error')); const response = await client.files['upload-policy'].$post({ json: mockFileData }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(500); }); }); describe('GET /api/v1/files/{id}/url', () => { it('should generate file access URL successfully', async () => { const mockUrl = 'https://minio.example.com/presigned-url'; vi.mocked(mockFileService.getFileUrl).mockResolvedValue(mockUrl); const response = await client.files[':id']['url'].$get({ param: { id: 1 } }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(200); const result = await response.json(); expect(result).toEqual({ url: mockUrl }); expect(mockFileService.getFileUrl).toHaveBeenCalledWith(1); }); it('should return 404 when file not found', async () => { vi.mocked(mockFileService.getFileUrl).mockRejectedValue(new Error('文件不存在')); const response = await client.files[':id']['url'].$get({ param: { id: 999 } }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(404); }); }); describe('GET /api/v1/files/{id}/download', () => { it('should generate file download URL successfully', async () => { const mockDownloadInfo = { url: 'https://minio.example.com/download-url', filename: 'test.txt' }; vi.mocked(mockFileService.getFileDownloadUrl).mockResolvedValue(mockDownloadInfo); const response = await client.files[':id']['download'].$get({ param: { id: 1 } }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(200); const result = await response.json(); expect(result).toEqual(mockDownloadInfo); expect(mockFileService.getFileDownloadUrl).toHaveBeenCalledWith(1); }); it('should return 404 when file not found for download', async () => { vi.mocked(mockFileService.getFileDownloadUrl).mockRejectedValue(new Error('文件不存在')); const response = await client.files[':id']['download'].$get({ param: { id: 999 } }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(404); }); }); describe('DELETE /api/v1/files/{id}', () => { it('should delete file successfully', async () => { vi.mocked(mockFileService.deleteFile).mockResolvedValue(true); const response = await client.files[':id'].$delete({ param: { id: 1 } }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(200); const result = await response.json(); expect(result).toEqual({ success: true }); expect(mockFileService.deleteFile).toHaveBeenCalledWith(1); }); it('should return 404 when file not found for deletion', async () => { vi.mocked(mockFileService.deleteFile).mockRejectedValue(new Error('文件不存在')); const response = await client.files[':id'].$delete({ param: { id: 999 } }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(404); }); it('should handle deletion errors', async () => { vi.mocked(mockFileService.deleteFile).mockRejectedValue(new Error('删除失败')); const response = await client.files[':id'].$delete({ param: { id: 1 } }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(500); }); }); describe('POST /api/v1/files/multipart-policy', () => { it('should generate multipart upload policy successfully', async () => { const mockRequestData = { name: 'large-file.zip', type: 'application/zip', size: 1024 * 1024 * 100, // 100MB uploadUserId: 1, partCount: 5 }; const mockResponse = { file: { id: 1, ...mockRequestData, path: '1/test-uuid-123-large-file.zip', uploadTime: new Date(), createdAt: new Date(), updatedAt: new Date() }, uploadId: 'upload-123', uploadUrls: ['url1', 'url2', 'url3', 'url4', 'url5'], bucket: 'd8dai', key: '1/test-uuid-123-large-file.zip' }; vi.mocked(mockFileService.createMultipartUploadPolicy).mockResolvedValue(mockResponse); const response = await client.files['multipart-policy'].$post({ json: mockRequestData }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(200); const result = await response.json(); expect(result).toEqual(mockResponse); expect(mockFileService.createMultipartUploadPolicy).toHaveBeenCalledWith( { name: 'large-file.zip', type: 'application/zip', size: 104857600, uploadUserId: 1 }, 5 ); }); it('should validate multipart policy request data', async () => { const invalidData = { name: 'test.zip', // Missing required fields }; const response = await client.files['multipart-policy'].$post({ json: invalidData }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(400); }); }); describe('POST /api/v1/files/multipart-complete', () => { it('should complete multipart upload successfully', async () => { const mockCompleteData = { uploadId: 'upload-123', bucket: 'd8dai', key: '1/test-file.zip', parts: [ { partNumber: 1, etag: 'etag1' }, { partNumber: 2, etag: 'etag2' } ] }; const mockResponse = { fileId: 1, url: 'https://minio.example.com/file.zip', key: '1/test-file.zip', size: 2048 }; vi.mocked(mockFileService.completeMultipartUpload).mockResolvedValue(mockResponse); const response = await client.files['multipart-complete'].$post({ json: mockCompleteData }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(200); const result = await response.json(); expect(result).toEqual(mockResponse); expect(mockFileService.completeMultipartUpload).toHaveBeenCalledWith(mockCompleteData); }); it('should validate complete multipart request data', async () => { const invalidData = { uploadId: 'upload-123', // Missing required fields }; const response = await client.files['multipart-complete'].$post({ json: invalidData }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(400); }); it('should handle completion errors', async () => { const completeData = { uploadId: 'upload-123', bucket: 'd8dai', key: '1/test-file.zip', parts: [{ partNumber: 1, etag: 'etag1' }] }; vi.mocked(mockFileService.completeMultipartUpload).mockRejectedValue(new Error('Completion failed')); const response = await client.files['multipart-complete'].$post({ json: completeData }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(500); }); }); describe('CRUD Operations', () => { it('should list files successfully', async () => { const mockFiles = [ { id: 1, name: 'file1.txt', type: 'text/plain', size: 1024, uploadUserId: 1 }, { id: 2, name: 'file2.txt', type: 'text/plain', size: 2048, uploadUserId: 1 } ]; vi.spyOn(mockFileService, 'getList').mockResolvedValue(mockFiles as File[]); const response = await client.files.$get({ query: {} }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(200); const result = await response.json(); expect(result).toEqual(mockFiles); }); it('should get file by ID successfully', async () => { const mockFile = { id: 1, name: 'file.txt', type: 'text/plain', size: 1024, uploadUserId: 1 }; vi.spyOn(mockFileService, 'getById').mockResolvedValue(mockFile as File); const response = await client.files[':id'].$get({ param: { id: 1 } }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(200); const result = await response.json(); expect(result).toEqual(mockFile); }); it('should search files successfully', async () => { const mockFiles = [ { id: 1, name: 'document.pdf', type: 'application/pdf', size: 1024, uploadUserId: 1 } ]; vi.spyOn(mockFileService, 'getList').mockResolvedValue(mockFiles as File[]); const response = await client.files.$get({ query: { keyword: 'document' } }, { headers: { 'Authorization': 'Bearer test-token' } }); expect(response.status).toBe(200); const result = await response.json(); expect(result).toEqual(mockFiles); expect(mockFileService.getList).toHaveBeenCalledWith(1, 10, 'document', ['name', 'type', 'description'], undefined, [], {}, undefined); }); }); });