import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { testClient } from 'hono/testing'; import { FileService } from '@/server/modules/files/file.service'; import { authMiddleware } from '@/server/middleware/auth.middleware'; import { fileApiRoutes } from '@/server/api'; import { ConcreteCrudService } from '@/server/utils/concrete-crud.service'; vi.mock('@/server/modules/files/file.service'); vi.mock('@/server/middleware/auth.middleware'); vi.mock('@/server/data-source'); vi.mock('@/server/utils/concrete-crud.service'); 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 (c, next) => { const authHeader = c.req.header('Authorization'); if (!authHeader) { return c.json({ message: 'Authorization header missing' }, 401); } c.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 as any }, { 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', path: '/uploads/test.txt', uploadUserId: 1 }; const mockCreateFile = vi.fn().mockRejectedValue(new Error('Service error')); vi.mocked(FileService).mockImplementation(() => ({ createFile: mockCreateFile } as unknown as FileService)); const response = await client.files['upload-policy'].$post({ json: mockFileData as any }, { 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'; const mockGetFileUrl = vi.fn().mockResolvedValue(mockUrl); vi.mocked(FileService).mockImplementation(() => ({ getFileUrl: mockGetFileUrl } as unknown as FileService)); 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 }); }); it('should return 404 when file not found', async () => { const mockGetFileUrl = vi.fn().mockRejectedValue(new Error('文件不存在')); vi.mocked(FileService).mockImplementation(() => ({ getFileUrl: mockGetFileUrl } as unknown as FileService)); 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' }; const mockGetFileDownloadUrl = vi.fn().mockResolvedValue(mockDownloadInfo); vi.mocked(FileService).mockImplementation(() => ({ getFileDownloadUrl: mockGetFileDownloadUrl } as unknown as FileService)); 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(mockGetFileDownloadUrl).toHaveBeenCalledWith(1); }); it('should return 404 when file not found for download', async () => { const mockGetFileDownloadUrl = vi.fn().mockRejectedValue(new Error('文件不存在')); vi.mocked(FileService).mockImplementation(() => ({ getFileDownloadUrl: mockGetFileDownloadUrl } as unknown as FileService)); 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 () => { const mockDeleteFile = vi.fn().mockResolvedValue(true); vi.mocked(FileService).mockImplementation(() => ({ deleteFile: mockDeleteFile } as unknown as FileService)); 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, message: '文件删除成功' }); expect(mockDeleteFile).toHaveBeenCalledWith(1); }); it('should return 404 when file not found for deletion', async () => { const mockDeleteFile = vi.fn().mockRejectedValue(new Error('文件不存在')); vi.mocked(FileService).mockImplementation(() => ({ deleteFile: mockDeleteFile } as unknown as FileService)); 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 () => { const mockDeleteFile = vi.fn().mockRejectedValue(new Error('删除失败')); vi.mocked(FileService).mockImplementation(() => ({ deleteFile: mockDeleteFile } as unknown as FileService)); 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 mockServiceResponse = { file: { id: 1, name: 'large-file.zip', type: 'application/zip', size: 104857600, uploadUserId: 1, path: '1/test-uuid-123-large-file.zip', description: null, 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' }; const mockCreateMultipartUploadPolicy = vi.fn().mockResolvedValue(mockServiceResponse); vi.mocked(FileService).mockImplementation(() => ({ createMultipartUploadPolicy: mockCreateMultipartUploadPolicy } as unknown as FileService)); 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({ uploadId: 'upload-123', bucket: 'd8dai', key: '1/test-uuid-123-large-file.zip', host: 'http://undefined:undefined', partUrls: ['url1', 'url2', 'url3', 'url4', 'url5'] }); expect(mockCreateMultipartUploadPolicy).toHaveBeenCalledWith( { fileKey: 'large-file.zip', totalSize: 104857600, partSize: 20971520, name: 'large-file.zip', type: 'application/zip', 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 as any }, { 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, host: 'http://undefined:undefined', bucket: 'd8dai' }; const mockCompleteMultipartUpload = vi.fn().mockResolvedValue(mockResponse); vi.mocked(FileService).mockImplementation(() => ({ completeMultipartUpload: mockCompleteMultipartUpload } as unknown as FileService)); 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(mockCompleteMultipartUpload).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 as any }, { 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' }] }; const mockCompleteMultipartUpload = vi.fn().mockRejectedValue(new Error('Completion failed')); vi.mocked(FileService).mockImplementation(() => ({ completeMultipartUpload: mockCompleteMultipartUpload } as unknown as FileService)); 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, path: '/uploads/file1.txt', fullUrl: 'https://minio.example.com/d8dai/uploads/file1.txt', description: null, uploadUserId: 1, uploadUser: user1Response, uploadTime: new Date(), lastUpdated: null, createdAt: new Date(), updatedAt: new Date() }, { id: 2, name: 'file2.txt', type: 'text/plain', size: 2048, path: '/uploads/file2.txt', fullUrl: 'https://minio.example.com/d8dai/uploads/file2.txt', description: null, uploadUserId: 1, uploadUser: user1Response, uploadTime: new Date(), lastUpdated: null, createdAt: new Date(), updatedAt: new Date() } ]; // 设置ConcreteCrudService的mock返回数据 vi.mocked(ConcreteCrudService).mockImplementation(() => ({ getList: vi.fn().mockResolvedValue([mockFiles, mockFiles.length]) } as unknown as ConcreteCrudService)); const response = await client.files.$get({ query: {} }, { 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({ data: mockFiles.map(file => ({ ...file, createdAt: file.createdAt.toISOString(), updatedAt: file.updatedAt.toISOString(), uploadTime: file.uploadTime.toISOString() })), pagination: { current: 1, pageSize: 10, total: mockFiles.length } }); }); it('should get file by ID successfully', async () => { const mockFile = { id: 1, name: 'file.txt', type: 'text/plain', size: 1024, path: '/uploads/file.txt', fullUrl: 'https://minio.example.com/d8dai/uploads/file.txt', description: null, uploadUserId: 1, uploadUser: user1Response, uploadTime: new Date(), lastUpdated: null, createdAt: new Date(), updatedAt: new Date() }; // 设置ConcreteCrudService的mock返回数据 vi.mocked(ConcreteCrudService).mockImplementation(() => ({ getById: vi.fn().mockResolvedValue(mockFile) } as unknown as ConcreteCrudService)); const response = await client.files[':id'].$get({ param: { id: 1 } }, { 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({ ...mockFile, createdAt: mockFile.createdAt.toISOString(), updatedAt: mockFile.updatedAt.toISOString(), uploadTime: mockFile.uploadTime.toISOString() }); }); it('should search files successfully', async () => { const mockFiles = [ { id: 1, name: 'document.pdf', type: 'application/pdf', size: 1024, path: '/uploads/document.pdf', fullUrl: 'https://minio.example.com/d8dai/uploads/document.pdf', description: null, uploadUserId: 1, uploadUser: user1Response, uploadTime: new Date(), lastUpdated: null, createdAt: new Date(), updatedAt: new Date() } ]; // 设置ConcreteCrudService的mock返回数据 vi.mocked(ConcreteCrudService).mockImplementation(() => ({ getList: vi.fn().mockResolvedValue([mockFiles, mockFiles.length]) } as unknown as ConcreteCrudService)); 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({ data: mockFiles.map(file => ({ ...file, createdAt: file.createdAt.toISOString(), updatedAt: file.updatedAt.toISOString(), uploadTime: file.uploadTime.toISOString() })), pagination: { current: 1, pageSize: 10, total: mockFiles.length } }); expect(vi.mocked(ConcreteCrudService).mock.results[0].value.getList).toHaveBeenCalledWith(1, 10, 'document', ['name', 'type', 'description'], undefined, ['uploadUser'], { id: 'DESC' }, undefined); }); }); });