import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { testClient } from 'hono/testing'; import { DataSource } from 'typeorm'; import { FileService } from '@/server/modules/files/file.service'; import { authMiddleware } from '@/server/middleware/auth.middleware'; import { fileApiRoutes } from '@/server/api'; vi.mock('@/server/modules/files/file.service'); vi.mock('@/server/middleware/auth.middleware'); describe('File API Integration Tests', () => { let client: ReturnType>['api']['v1']; const user1 = { id: 1, username: 'testuser', password: 'password123', phone: null, email: null, nickname: null, name: null, avatarFileId: null, avatarFile: null, isDisabled: 0, isDeleted: 0, roles: [], createdAt: new Date(), updatedAt: new Date() }; const user1Response = { ...user1, createdAt: (user1.createdAt).toISOString(), updatedAt: (user1.updatedAt).toISOString() } beforeEach(async () => { vi.clearAllMocks(); // Mock auth middleware to bypass authentication vi.mocked(authMiddleware).mockImplementation(async (_, next) => { _.set('user', user1) await next(); }); 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()).toISOString(), createdAt: (new Date()).toISOString(), updatedAt: (new Date()).toISOString(), fullUrl: 'https://minio.example.com/d8dai/1/test-uuid-123-test.txt', uploadUser: user1Response, lastUpdated: null }, uploadPolicy: { 'x-amz-algorithm': 'AWS4-HMAC-SHA256', 'x-amz-credential': 'test-credential', 'x-amz-date': '20250101T120000Z', 'x-amz-security-token': 'test-token', policy: 'test-policy', 'x-amz-signature': 'test-signature', host: 'https://minio.example.com', key: '1/test-uuid-123-test.txt', bucket: 'd8dai' } }; const mockCreateFile = vi.fn().mockResolvedValue(mockResponse); vi.mocked(FileService).mockImplementation(() => ({ createFile: mockCreateFile } as unknown as FileService)); 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(mockCreateFile).toHaveBeenCalledWith({ ...mockFileData, uploadTime: expect.any(Date), uploadUserId: 1 }); }); 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 = { // fileKey: 'large-file.zip', // totalSize: 1024 * 1024 * 100, // 100MB // partSize: 1024 * 1024 * 20, // 20MB // name: 'large-file.zip', // type: 'application/zip', // uploadUserId: 1 // }; // const mockResponse = { // file: { // id: 1, // ...mockRequestData, // path: '1/test-uuid-123-large-file.zip', // description: null, // uploadUser: {} as any, // uploadTime: new Date(), // lastUpdated: null, // createdAt: new Date(), // updatedAt: new Date(), // fullUrl: Promise.resolve('https://minio.example.com/d8dai/1/test-uuid-123-large-file.zip') // }, // 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: fileKey, totalSize, partSize // }; // 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: bucket, key, parts // }; // 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[], mockFiles.length]); // 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[], mockFiles.length]); // 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); // }); // }); });