files.integration.test.ts 15 KB

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