files.integration.test.ts 15 KB

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