files.integration.test.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
  2. import { testClient } from 'hono/testing';
  3. import { DataSource } from 'typeorm';
  4. import { File } from '@/server/modules/files/file.entity';
  5. import { FileService } from '@/server/modules/files/file.service';
  6. import { MinioService } from '@/server/modules/files/minio.service';
  7. import { authMiddleware } from '@/server/middleware/auth.middleware';
  8. import { fileApiRoutes } from '@/server/api';
  9. // Mock dependencies
  10. vi.mock('@/server/modules/files/file.service', () => ({
  11. FileService: vi.fn().mockImplementation(() => ({
  12. createFile: vi.fn()
  13. }))
  14. }));
  15. vi.mock('@/server/modules/files/minio.service');
  16. vi.mock('@/server/middleware/auth.middleware');
  17. describe('File API Integration Tests', () => {
  18. let client: ReturnType<typeof testClient<typeof fileApiRoutes>>['api']['v1'];
  19. let mockFileService: FileService;
  20. let mockDataSource: DataSource;
  21. beforeEach(async () => {
  22. vi.clearAllMocks();
  23. mockDataSource = {} as DataSource;
  24. // Mock auth middleware to bypass authentication
  25. vi.mocked(authMiddleware).mockImplementation(async (_, next) => {
  26. await next();
  27. });
  28. // Get the mocked FileService instance
  29. mockFileService = new FileService(mockDataSource);
  30. client = testClient(fileApiRoutes).api.v1;
  31. });
  32. afterEach(() => {
  33. vi.clearAllMocks();
  34. });
  35. describe('POST /api/v1/files/upload-policy', () => {
  36. it('should generate upload policy successfully', async () => {
  37. const mockFileData = {
  38. name: 'test.txt',
  39. type: 'text/plain',
  40. size: 1024,
  41. path: '/uploads/test.txt',
  42. description: 'Test file',
  43. uploadUserId: 1
  44. };
  45. const mockResponse = {
  46. file: {
  47. id: 1,
  48. ...mockFileData,
  49. path: '1/test-uuid-123-test.txt',
  50. uploadTime: new Date(),
  51. createdAt: new Date(),
  52. updatedAt: new Date()
  53. },
  54. uploadPolicy: {
  55. host: 'https://minio.example.com',
  56. key: '1/test-uuid-123-test.txt',
  57. bucket: 'd8dai'
  58. }
  59. };
  60. mockFileService.createFile = vi.fn().mockResolvedValue(mockResponse);
  61. const response = await client.files['upload-policy'].$post({
  62. json: mockFileData
  63. },
  64. {
  65. headers: {
  66. 'Authorization': 'Bearer test-token'
  67. }
  68. });
  69. if (response.status !== 200) {
  70. const error = await response.json();
  71. console.debug('Error response:', JSON.stringify(error, null, 2));
  72. console.debug('Response status:', response.status);
  73. }
  74. expect(response.status).toBe(200);
  75. const result = await response.json();
  76. expect(result).toEqual(mockResponse);
  77. expect(mockFileService.createFile).toHaveBeenCalledWith(mockFileData);
  78. });
  79. it('should return 400 for invalid request data', async () => {
  80. const invalidData = {
  81. name: '', // Empty name
  82. type: 'text/plain'
  83. };
  84. const response = await client.files['upload-policy'].$post({
  85. json: invalidData
  86. },
  87. {
  88. headers: {
  89. 'Authorization': 'Bearer test-token'
  90. }
  91. });
  92. expect(response.status).toBe(400);
  93. });
  94. it('should handle service errors gracefully', async () => {
  95. const mockFileData = {
  96. name: 'test.txt',
  97. type: 'text/plain',
  98. uploadUserId: 1
  99. };
  100. mockFileService.createFile = vi.fn().mockRejectedValue(new Error('Service error'));
  101. const response = await client.files['upload-policy'].$post({
  102. json: mockFileData
  103. },
  104. {
  105. headers: {
  106. 'Authorization': 'Bearer test-token'
  107. }
  108. });
  109. expect(response.status).toBe(500);
  110. });
  111. });
  112. describe('GET /api/v1/files/{id}/url', () => {
  113. it('should generate file access URL successfully', async () => {
  114. const mockUrl = 'https://minio.example.com/presigned-url';
  115. vi.mocked(mockFileService.getFileUrl).mockResolvedValue(mockUrl);
  116. const response = await client.files[':id']['url'].$get({
  117. param: { id: 1 }
  118. },
  119. {
  120. headers: {
  121. 'Authorization': 'Bearer test-token'
  122. }
  123. });
  124. expect(response.status).toBe(200);
  125. const result = await response.json();
  126. expect(result).toEqual({ url: mockUrl });
  127. expect(mockFileService.getFileUrl).toHaveBeenCalledWith(1);
  128. });
  129. it('should return 404 when file not found', async () => {
  130. vi.mocked(mockFileService.getFileUrl).mockRejectedValue(new Error('文件不存在'));
  131. const response = await client.files[':id']['url'].$get({
  132. param: { id: 999 }
  133. },
  134. {
  135. headers: {
  136. 'Authorization': 'Bearer test-token'
  137. }
  138. });
  139. expect(response.status).toBe(404);
  140. });
  141. });
  142. describe('GET /api/v1/files/{id}/download', () => {
  143. it('should generate file download URL successfully', async () => {
  144. const mockDownloadInfo = {
  145. url: 'https://minio.example.com/download-url',
  146. filename: 'test.txt'
  147. };
  148. vi.mocked(mockFileService.getFileDownloadUrl).mockResolvedValue(mockDownloadInfo);
  149. const response = await client.files[':id']['download'].$get({
  150. param: { id: 1 }
  151. },
  152. {
  153. headers: {
  154. 'Authorization': 'Bearer test-token'
  155. }
  156. });
  157. expect(response.status).toBe(200);
  158. const result = await response.json();
  159. expect(result).toEqual(mockDownloadInfo);
  160. expect(mockFileService.getFileDownloadUrl).toHaveBeenCalledWith(1);
  161. });
  162. it('should return 404 when file not found for download', async () => {
  163. vi.mocked(mockFileService.getFileDownloadUrl).mockRejectedValue(new Error('文件不存在'));
  164. const response = await client.files[':id']['download'].$get({
  165. param: { id: 999 }
  166. },
  167. {
  168. headers: {
  169. 'Authorization': 'Bearer test-token'
  170. }
  171. });
  172. expect(response.status).toBe(404);
  173. });
  174. });
  175. describe('DELETE /api/v1/files/{id}', () => {
  176. it('should delete file successfully', async () => {
  177. vi.mocked(mockFileService.deleteFile).mockResolvedValue(true);
  178. const response = await client.files[':id'].$delete({
  179. param: { id: 1 }
  180. },
  181. {
  182. headers: {
  183. 'Authorization': 'Bearer test-token'
  184. }
  185. });
  186. expect(response.status).toBe(200);
  187. const result = await response.json();
  188. expect(result).toEqual({ success: true });
  189. expect(mockFileService.deleteFile).toHaveBeenCalledWith(1);
  190. });
  191. it('should return 404 when file not found for deletion', async () => {
  192. vi.mocked(mockFileService.deleteFile).mockRejectedValue(new Error('文件不存在'));
  193. const response = await client.files[':id'].$delete({
  194. param: { id: 999 }
  195. },
  196. {
  197. headers: {
  198. 'Authorization': 'Bearer test-token'
  199. }
  200. });
  201. expect(response.status).toBe(404);
  202. });
  203. it('should handle deletion errors', async () => {
  204. vi.mocked(mockFileService.deleteFile).mockRejectedValue(new Error('删除失败'));
  205. const response = await client.files[':id'].$delete({
  206. param: { id: 1 }
  207. },
  208. {
  209. headers: {
  210. 'Authorization': 'Bearer test-token'
  211. }
  212. });
  213. expect(response.status).toBe(500);
  214. });
  215. });
  216. describe('POST /api/v1/files/multipart-policy', () => {
  217. it('should generate multipart upload policy successfully', async () => {
  218. const mockRequestData = {
  219. fileKey: 'large-file.zip',
  220. totalSize: 1024 * 1024 * 100, // 100MB
  221. partSize: 1024 * 1024 * 20, // 20MB
  222. name: 'large-file.zip',
  223. type: 'application/zip',
  224. uploadUserId: 1
  225. };
  226. const mockResponse = {
  227. file: {
  228. id: 1,
  229. ...mockRequestData,
  230. path: '1/test-uuid-123-large-file.zip',
  231. description: null,
  232. uploadUser: {} as any,
  233. uploadTime: new Date(),
  234. lastUpdated: null,
  235. createdAt: new Date(),
  236. updatedAt: new Date(),
  237. fullUrl: Promise.resolve('https://minio.example.com/d8dai/1/test-uuid-123-large-file.zip')
  238. },
  239. uploadId: 'upload-123',
  240. uploadUrls: ['url1', 'url2', 'url3', 'url4', 'url5'],
  241. bucket: 'd8dai',
  242. key: '1/test-uuid-123-large-file.zip'
  243. };
  244. vi.mocked(mockFileService.createMultipartUploadPolicy).mockResolvedValue(mockResponse);
  245. const response = await client.files['multipart-policy'].$post({
  246. json: mockRequestData
  247. },
  248. {
  249. headers: {
  250. 'Authorization': 'Bearer test-token'
  251. }
  252. });
  253. expect(response.status).toBe(200);
  254. const result = await response.json();
  255. expect(result).toEqual(mockResponse);
  256. expect(mockFileService.createMultipartUploadPolicy).toHaveBeenCalledWith(
  257. {
  258. name: 'large-file.zip',
  259. type: 'application/zip',
  260. size: 104857600,
  261. uploadUserId: 1
  262. },
  263. 5
  264. );
  265. });
  266. it('should validate multipart policy request data', async () => {
  267. const invalidData = {
  268. name: 'test.zip',
  269. // Missing required fields: fileKey, totalSize, partSize
  270. };
  271. const response = await client.files['multipart-policy'].$post({
  272. json: invalidData
  273. },
  274. {
  275. headers: {
  276. 'Authorization': 'Bearer test-token'
  277. }
  278. });
  279. expect(response.status).toBe(400);
  280. });
  281. });
  282. describe('POST /api/v1/files/multipart-complete', () => {
  283. it('should complete multipart upload successfully', async () => {
  284. const mockCompleteData = {
  285. uploadId: 'upload-123',
  286. bucket: 'd8dai',
  287. key: '1/test-file.zip',
  288. parts: [
  289. { partNumber: 1, etag: 'etag1' },
  290. { partNumber: 2, etag: 'etag2' }
  291. ]
  292. };
  293. const mockResponse = {
  294. fileId: 1,
  295. url: 'https://minio.example.com/file.zip',
  296. key: '1/test-file.zip',
  297. size: 2048
  298. };
  299. vi.mocked(mockFileService.completeMultipartUpload).mockResolvedValue(mockResponse);
  300. const response = await client.files['multipart-complete'].$post({
  301. json: mockCompleteData
  302. },
  303. {
  304. headers: {
  305. 'Authorization': 'Bearer test-token'
  306. }
  307. });
  308. expect(response.status).toBe(200);
  309. const result = await response.json();
  310. expect(result).toEqual(mockResponse);
  311. expect(mockFileService.completeMultipartUpload).toHaveBeenCalledWith(mockCompleteData);
  312. });
  313. it('should validate complete multipart request data', async () => {
  314. const invalidData = {
  315. uploadId: 'upload-123',
  316. // Missing required fields: bucket, key, parts
  317. };
  318. const response = await client.files['multipart-complete'].$post({
  319. json: invalidData
  320. },
  321. {
  322. headers: {
  323. 'Authorization': 'Bearer test-token'
  324. }
  325. });
  326. expect(response.status).toBe(400);
  327. });
  328. it('should handle completion errors', async () => {
  329. const completeData = {
  330. uploadId: 'upload-123',
  331. bucket: 'd8dai',
  332. key: '1/test-file.zip',
  333. parts: [{ partNumber: 1, etag: 'etag1' }]
  334. };
  335. vi.mocked(mockFileService.completeMultipartUpload).mockRejectedValue(new Error('Completion failed'));
  336. const response = await client.files['multipart-complete'].$post({
  337. json: completeData
  338. },
  339. {
  340. headers: {
  341. 'Authorization': 'Bearer test-token'
  342. }
  343. });
  344. expect(response.status).toBe(500);
  345. });
  346. });
  347. describe('CRUD Operations', () => {
  348. it('should list files successfully', async () => {
  349. const mockFiles = [
  350. {
  351. id: 1,
  352. name: 'file1.txt',
  353. type: 'text/plain',
  354. size: 1024,
  355. uploadUserId: 1
  356. },
  357. {
  358. id: 2,
  359. name: 'file2.txt',
  360. type: 'text/plain',
  361. size: 2048,
  362. uploadUserId: 1
  363. }
  364. ];
  365. vi.spyOn(mockFileService, 'getList').mockResolvedValue([mockFiles as File[], mockFiles.length]);
  366. const response = await client.files.$get({
  367. query: {}
  368. },
  369. {
  370. headers: {
  371. 'Authorization': 'Bearer test-token'
  372. }
  373. });
  374. expect(response.status).toBe(200);
  375. const result = await response.json();
  376. expect(result).toEqual(mockFiles);
  377. });
  378. it('should get file by ID successfully', async () => {
  379. const mockFile = {
  380. id: 1,
  381. name: 'file.txt',
  382. type: 'text/plain',
  383. size: 1024,
  384. uploadUserId: 1
  385. };
  386. vi.spyOn(mockFileService, 'getById').mockResolvedValue(mockFile as File);
  387. const response = await client.files[':id'].$get({
  388. param: { id: 1 }
  389. },
  390. {
  391. headers: {
  392. 'Authorization': 'Bearer test-token'
  393. }
  394. });
  395. expect(response.status).toBe(200);
  396. const result = await response.json();
  397. expect(result).toEqual(mockFile);
  398. });
  399. it('should search files successfully', async () => {
  400. const mockFiles = [
  401. {
  402. id: 1,
  403. name: 'document.pdf',
  404. type: 'application/pdf',
  405. size: 1024,
  406. uploadUserId: 1
  407. }
  408. ];
  409. vi.spyOn(mockFileService, 'getList').mockResolvedValue([mockFiles as File[], mockFiles.length]);
  410. const response = await client.files.$get({
  411. query: { keyword: 'document' }
  412. },
  413. {
  414. headers: {
  415. 'Authorization': 'Bearer test-token'
  416. }
  417. });
  418. expect(response.status).toBe(200);
  419. const result = await response.json();
  420. expect(result).toEqual(mockFiles);
  421. expect(mockFileService.getList).toHaveBeenCalledWith(1, 10, 'document', ['name', 'type', 'description'], undefined, [], {}, undefined);
  422. });
  423. });
  424. });