files.integration.test.ts 16 KB

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