files.integration.test.ts 15 KB

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