2
0

files.integration.test.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
  2. import { testClient } from 'hono/testing';
  3. import { FileService } from '@d8d/server/modules/files/file.service';
  4. import { authMiddleware } from '@d8d/server/middleware/auth.middleware';
  5. import { fileApiRoutes } from '@d8d/server/api';
  6. import { ConcreteCrudService } from '@d8d/server/utils/concrete-crud.service';
  7. vi.mock('@d8d/server/modules/files/file.service');
  8. vi.mock('@d8d/server/middleware/auth.middleware');
  9. vi.mock('@d8d/server/data-source');
  10. vi.mock('@d8d/server/utils/concrete-crud.service');
  11. describe('File API Integration Tests', () => {
  12. let client: ReturnType<typeof testClient<typeof fileApiRoutes>>['api']['v1'];
  13. const user1 = {
  14. id: 1,
  15. username: 'testuser',
  16. password: 'password123',
  17. phone: null,
  18. email: null,
  19. nickname: null,
  20. name: null,
  21. avatarFileId: null,
  22. avatarFile: null,
  23. isDisabled: 0,
  24. isDeleted: 0,
  25. registrationSource: 'web',
  26. roles: [],
  27. createdAt: new Date(),
  28. updatedAt: new Date()
  29. };
  30. const user1Response = {
  31. ...user1,
  32. createdAt: (user1.createdAt).toISOString(),
  33. updatedAt: (user1.updatedAt).toISOString()
  34. }
  35. beforeEach(async () => {
  36. vi.clearAllMocks();
  37. // Mock auth middleware to bypass authentication
  38. vi.mocked(authMiddleware).mockImplementation(async (c: any, next: any) => {
  39. const authHeader = c.req.header('Authorization');
  40. if (!authHeader) {
  41. return c.json({ message: 'Authorization header missing' }, 401);
  42. }
  43. c.set('user', user1)
  44. await next();
  45. });
  46. client = testClient(fileApiRoutes).api.v1;
  47. });
  48. afterEach(() => {
  49. vi.clearAllMocks();
  50. });
  51. describe('POST /api/v1/files/upload-policy', () => {
  52. it('should generate upload policy successfully', async () => {
  53. const mockFileData = {
  54. name: 'test.txt',
  55. type: 'text/plain',
  56. size: 1024,
  57. path: '/uploads/test.txt',
  58. description: 'Test file',
  59. uploadUserId: 1
  60. };
  61. const mockResponse = {
  62. file: {
  63. id: 1,
  64. ...mockFileData,
  65. path: '1/test-uuid-123-test.txt',
  66. uploadTime: (new Date()).toISOString(),
  67. createdAt: (new Date()).toISOString(),
  68. updatedAt: (new Date()).toISOString(),
  69. fullUrl: 'https://minio.example.com/d8dai/1/test-uuid-123-test.txt',
  70. uploadUser: user1Response,
  71. lastUpdated: null
  72. },
  73. uploadPolicy: {
  74. 'x-amz-algorithm': 'AWS4-HMAC-SHA256',
  75. 'x-amz-credential': 'test-credential',
  76. 'x-amz-date': '20250101T120000Z',
  77. 'x-amz-security-token': 'test-token',
  78. policy: 'test-policy',
  79. 'x-amz-signature': 'test-signature',
  80. host: 'https://minio.example.com',
  81. key: '1/test-uuid-123-test.txt',
  82. bucket: 'd8dai'
  83. }
  84. };
  85. const mockCreateFile = vi.fn().mockResolvedValue(mockResponse);
  86. vi.mocked(FileService).mockImplementation(() => ({
  87. createFile: mockCreateFile
  88. } as unknown as FileService));
  89. const response = await client.files['upload-policy'].$post({
  90. json: mockFileData
  91. },
  92. {
  93. headers: {
  94. 'Authorization': 'Bearer test-token'
  95. }
  96. });
  97. if (response.status !== 200) {
  98. const error = await response.json();
  99. console.debug('Error response:', JSON.stringify(error, null, 2));
  100. console.debug('Response status:', response.status);
  101. }
  102. expect(response.status).toBe(200);
  103. const result = await response.json();
  104. expect(result).toEqual(mockResponse);
  105. expect(mockCreateFile).toHaveBeenCalledWith({
  106. ...mockFileData,
  107. uploadTime: expect.any(Date),
  108. uploadUserId: 1
  109. });
  110. });
  111. it('should return 400 for invalid request data', async () => {
  112. const invalidData = {
  113. name: '', // Empty name
  114. type: 'text/plain'
  115. };
  116. const response = await client.files['upload-policy'].$post({
  117. json: invalidData as any
  118. },
  119. {
  120. headers: {
  121. 'Authorization': 'Bearer test-token'
  122. }
  123. });
  124. expect(response.status).toBe(400);
  125. });
  126. it('should handle service errors gracefully', async () => {
  127. const mockFileData = {
  128. name: 'test.txt',
  129. type: 'text/plain',
  130. path: '/uploads/test.txt',
  131. uploadUserId: 1
  132. };
  133. const mockCreateFile = vi.fn().mockRejectedValue(new Error('Service error'));
  134. vi.mocked(FileService).mockImplementation(() => ({
  135. createFile: mockCreateFile
  136. } as unknown as FileService));
  137. const response = await client.files['upload-policy'].$post({
  138. json: mockFileData as any
  139. },
  140. {
  141. headers: {
  142. 'Authorization': 'Bearer test-token'
  143. }
  144. });
  145. expect(response.status).toBe(500);
  146. });
  147. });
  148. describe('GET /api/v1/files/{id}/url', () => {
  149. it('should generate file access URL successfully', async () => {
  150. const mockUrl = 'https://minio.example.com/presigned-url';
  151. const mockGetFileUrl = vi.fn().mockResolvedValue(mockUrl);
  152. vi.mocked(FileService).mockImplementation(() => ({
  153. getFileUrl: mockGetFileUrl
  154. } as unknown as FileService));
  155. const response = await client.files[':id']['url'].$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({ url: mockUrl });
  166. });
  167. it('should return 404 when file not found', async () => {
  168. const mockGetFileUrl = vi.fn().mockRejectedValue(new Error('文件不存在'));
  169. vi.mocked(FileService).mockImplementation(() => ({
  170. getFileUrl: mockGetFileUrl
  171. } as unknown as FileService));
  172. const response = await client.files[':id']['url'].$get({
  173. param: { id: 999 }
  174. },
  175. {
  176. headers: {
  177. 'Authorization': 'Bearer test-token'
  178. }
  179. });
  180. expect(response.status).toBe(404);
  181. });
  182. });
  183. describe('GET /api/v1/files/{id}/download', () => {
  184. it('should generate file download URL successfully', async () => {
  185. const mockDownloadInfo = {
  186. url: 'https://minio.example.com/download-url',
  187. filename: 'test.txt'
  188. };
  189. const mockGetFileDownloadUrl = vi.fn().mockResolvedValue(mockDownloadInfo);
  190. vi.mocked(FileService).mockImplementation(() => ({
  191. getFileDownloadUrl: mockGetFileDownloadUrl
  192. } as unknown as FileService));
  193. const response = await client.files[':id']['download'].$get({
  194. param: { id: 1 }
  195. },
  196. {
  197. headers: {
  198. 'Authorization': 'Bearer test-token'
  199. }
  200. });
  201. expect(response.status).toBe(200);
  202. const result = await response.json();
  203. expect(result).toEqual(mockDownloadInfo);
  204. expect(mockGetFileDownloadUrl).toHaveBeenCalledWith(1);
  205. });
  206. it('should return 404 when file not found for download', async () => {
  207. const mockGetFileDownloadUrl = vi.fn().mockRejectedValue(new Error('文件不存在'));
  208. vi.mocked(FileService).mockImplementation(() => ({
  209. getFileDownloadUrl: mockGetFileDownloadUrl
  210. } as unknown as FileService));
  211. const response = await client.files[':id']['download'].$get({
  212. param: { id: 999 }
  213. },
  214. {
  215. headers: {
  216. 'Authorization': 'Bearer test-token'
  217. }
  218. });
  219. expect(response.status).toBe(404);
  220. });
  221. });
  222. describe('DELETE /api/v1/files/{id}', () => {
  223. it('should delete file successfully', async () => {
  224. const mockDeleteFile = vi.fn().mockResolvedValue(true);
  225. vi.mocked(FileService).mockImplementation(() => ({
  226. deleteFile: mockDeleteFile
  227. } as unknown as FileService));
  228. const response = await client.files[':id'].$delete({
  229. param: { id: 1 }
  230. },
  231. {
  232. headers: {
  233. 'Authorization': 'Bearer test-token'
  234. }
  235. });
  236. expect(response.status).toBe(200);
  237. const result = await response.json();
  238. expect(result).toEqual({ success: true, message: '文件删除成功' });
  239. expect(mockDeleteFile).toHaveBeenCalledWith(1);
  240. });
  241. it('should return 404 when file not found for deletion', async () => {
  242. const mockDeleteFile = vi.fn().mockRejectedValue(new Error('文件不存在'));
  243. vi.mocked(FileService).mockImplementation(() => ({
  244. deleteFile: mockDeleteFile
  245. } as unknown as FileService));
  246. const response = await client.files[':id'].$delete({
  247. param: { id: 999 }
  248. },
  249. {
  250. headers: {
  251. 'Authorization': 'Bearer test-token'
  252. }
  253. });
  254. expect(response.status).toBe(404);
  255. });
  256. it('should handle deletion errors', async () => {
  257. const mockDeleteFile = vi.fn().mockRejectedValue(new Error('删除失败'));
  258. vi.mocked(FileService).mockImplementation(() => ({
  259. deleteFile: mockDeleteFile
  260. } as unknown as FileService));
  261. const response = await client.files[':id'].$delete({
  262. param: { id: 1 }
  263. },
  264. {
  265. headers: {
  266. 'Authorization': 'Bearer test-token'
  267. }
  268. });
  269. expect(response.status).toBe(500);
  270. });
  271. });
  272. describe('POST /api/v1/files/multipart-policy', () => {
  273. it('should generate multipart upload policy successfully', async () => {
  274. const mockRequestData = {
  275. fileKey: 'large-file.zip',
  276. totalSize: 1024 * 1024 * 100, // 100MB
  277. partSize: 1024 * 1024 * 20, // 20MB
  278. name: 'large-file.zip',
  279. type: 'application/zip',
  280. uploadUserId: 1
  281. };
  282. const mockServiceResponse = {
  283. file: {
  284. id: 1,
  285. name: 'large-file.zip',
  286. type: 'application/zip',
  287. size: 104857600,
  288. uploadUserId: 1,
  289. path: '1/test-uuid-123-large-file.zip',
  290. description: null,
  291. uploadTime: new Date(),
  292. lastUpdated: null,
  293. createdAt: new Date(),
  294. updatedAt: new Date(),
  295. fullUrl: Promise.resolve('https://minio.example.com/d8dai/1/test-uuid-123-large-file.zip')
  296. },
  297. uploadId: 'upload-123',
  298. uploadUrls: ['url1', 'url2', 'url3', 'url4', 'url5'],
  299. bucket: 'd8dai',
  300. key: '1/test-uuid-123-large-file.zip'
  301. };
  302. const mockCreateMultipartUploadPolicy = vi.fn().mockResolvedValue(mockServiceResponse);
  303. vi.mocked(FileService).mockImplementation(() => ({
  304. createMultipartUploadPolicy: mockCreateMultipartUploadPolicy
  305. } as unknown as FileService));
  306. const response = await client.files['multipart-policy'].$post({
  307. json: mockRequestData
  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({
  317. uploadId: 'upload-123',
  318. bucket: 'd8dai',
  319. key: '1/test-uuid-123-large-file.zip',
  320. host: 'http://undefined:undefined',
  321. partUrls: ['url1', 'url2', 'url3', 'url4', 'url5']
  322. });
  323. expect(mockCreateMultipartUploadPolicy).toHaveBeenCalledWith(
  324. {
  325. fileKey: 'large-file.zip',
  326. totalSize: 104857600,
  327. partSize: 20971520,
  328. name: 'large-file.zip',
  329. type: 'application/zip',
  330. uploadUserId: 1
  331. },
  332. 5
  333. );
  334. });
  335. it('should validate multipart policy request data', async () => {
  336. const invalidData = {
  337. name: 'test.zip'
  338. // Missing required fields: fileKey, totalSize, partSize
  339. };
  340. const response = await client.files['multipart-policy'].$post({
  341. json: invalidData as any
  342. },
  343. {
  344. headers: {
  345. 'Authorization': 'Bearer test-token'
  346. }
  347. });
  348. expect(response.status).toBe(400);
  349. });
  350. });
  351. describe('POST /api/v1/files/multipart-complete', () => {
  352. it('should complete multipart upload successfully', async () => {
  353. const mockCompleteData = {
  354. uploadId: 'upload-123',
  355. bucket: 'd8dai',
  356. key: '1/test-file.zip',
  357. parts: [
  358. { partNumber: 1, etag: 'etag1' },
  359. { partNumber: 2, etag: 'etag2' }
  360. ]
  361. };
  362. const mockResponse = {
  363. fileId: 1,
  364. url: 'https://minio.example.com/file.zip',
  365. key: '1/test-file.zip',
  366. size: 2048,
  367. host: 'http://undefined:undefined',
  368. bucket: 'd8dai'
  369. };
  370. const mockCompleteMultipartUpload = vi.fn().mockResolvedValue(mockResponse);
  371. vi.mocked(FileService).mockImplementation(() => ({
  372. completeMultipartUpload: mockCompleteMultipartUpload
  373. } as unknown as FileService));
  374. const response = await client.files['multipart-complete'].$post({
  375. json: mockCompleteData
  376. },
  377. {
  378. headers: {
  379. 'Authorization': 'Bearer test-token'
  380. }
  381. });
  382. expect(response.status).toBe(200);
  383. const result = await response.json();
  384. expect(result).toEqual(mockResponse);
  385. expect(mockCompleteMultipartUpload).toHaveBeenCalledWith(mockCompleteData);
  386. });
  387. it('should validate complete multipart request data', async () => {
  388. const invalidData = {
  389. uploadId: 'upload-123',
  390. // Missing required fields: bucket, key, parts
  391. };
  392. const response = await client.files['multipart-complete'].$post({
  393. json: invalidData as any
  394. },
  395. {
  396. headers: {
  397. 'Authorization': 'Bearer test-token'
  398. }
  399. });
  400. expect(response.status).toBe(400);
  401. });
  402. it('should handle completion errors', async () => {
  403. const completeData = {
  404. uploadId: 'upload-123',
  405. bucket: 'd8dai',
  406. key: '1/test-file.zip',
  407. parts: [{ partNumber: 1, etag: 'etag1' }]
  408. };
  409. const mockCompleteMultipartUpload = vi.fn().mockRejectedValue(new Error('Completion failed'));
  410. vi.mocked(FileService).mockImplementation(() => ({
  411. completeMultipartUpload: mockCompleteMultipartUpload
  412. } as unknown as FileService));
  413. const response = await client.files['multipart-complete'].$post({
  414. json: completeData
  415. },
  416. {
  417. headers: {
  418. 'Authorization': 'Bearer test-token'
  419. }
  420. });
  421. expect(response.status).toBe(500);
  422. });
  423. });
  424. describe('CRUD Operations', () => {
  425. it('should list files successfully', async () => {
  426. const mockFiles = [
  427. {
  428. id: 1,
  429. name: 'file1.txt',
  430. type: 'text/plain',
  431. size: 1024,
  432. path: '/uploads/file1.txt',
  433. fullUrl: 'https://minio.example.com/d8dai/uploads/file1.txt',
  434. description: null,
  435. uploadUserId: 1,
  436. uploadUser: user1Response,
  437. uploadTime: new Date(),
  438. lastUpdated: null,
  439. createdAt: new Date(),
  440. updatedAt: new Date()
  441. },
  442. {
  443. id: 2,
  444. name: 'file2.txt',
  445. type: 'text/plain',
  446. size: 2048,
  447. path: '/uploads/file2.txt',
  448. fullUrl: 'https://minio.example.com/d8dai/uploads/file2.txt',
  449. description: null,
  450. uploadUserId: 1,
  451. uploadUser: user1Response,
  452. uploadTime: new Date(),
  453. lastUpdated: null,
  454. createdAt: new Date(),
  455. updatedAt: new Date()
  456. }
  457. ];
  458. // 设置ConcreteCrudService的mock返回数据
  459. vi.mocked(ConcreteCrudService).mockImplementation(() => ({
  460. getList: vi.fn().mockResolvedValue([mockFiles, mockFiles.length])
  461. } as unknown as ConcreteCrudService<any>));
  462. const response = await client.files.$get({
  463. query: {}
  464. },
  465. {
  466. headers: {
  467. 'Authorization': 'Bearer test-token'
  468. }
  469. });
  470. if (response.status !== 200) {
  471. const error = await response.json();
  472. console.debug('Error response:', JSON.stringify(error, null, 2));
  473. console.debug('Response status:', response.status);
  474. }
  475. expect(response.status).toBe(200);
  476. const result = await response.json();
  477. expect(result).toEqual({
  478. data: mockFiles.map(file => ({
  479. ...file,
  480. createdAt: file.createdAt.toISOString(),
  481. updatedAt: file.updatedAt.toISOString(),
  482. uploadTime: file.uploadTime.toISOString()
  483. })),
  484. pagination: {
  485. current: 1,
  486. pageSize: 10,
  487. total: mockFiles.length
  488. }
  489. });
  490. });
  491. it('should get file by ID successfully', async () => {
  492. const mockFile = {
  493. id: 1,
  494. name: 'file.txt',
  495. type: 'text/plain',
  496. size: 1024,
  497. path: '/uploads/file.txt',
  498. fullUrl: 'https://minio.example.com/d8dai/uploads/file.txt',
  499. description: null,
  500. uploadUserId: 1,
  501. uploadUser: user1Response,
  502. uploadTime: new Date(),
  503. lastUpdated: null,
  504. createdAt: new Date(),
  505. updatedAt: new Date()
  506. };
  507. // 设置ConcreteCrudService的mock返回数据
  508. vi.mocked(ConcreteCrudService).mockImplementation(() => ({
  509. getById: vi.fn().mockResolvedValue(mockFile)
  510. } as unknown as ConcreteCrudService<any>));
  511. const response = await client.files[':id'].$get({
  512. param: { id: 1 }
  513. },
  514. {
  515. headers: {
  516. 'Authorization': 'Bearer test-token'
  517. }
  518. });
  519. if (response.status !== 200) {
  520. const error = await response.json();
  521. console.debug('Error response:', JSON.stringify(error, null, 2));
  522. console.debug('Response status:', response.status);
  523. }
  524. expect(response.status).toBe(200);
  525. const result = await response.json();
  526. expect(result).toEqual({
  527. ...mockFile,
  528. createdAt: mockFile.createdAt.toISOString(),
  529. updatedAt: mockFile.updatedAt.toISOString(),
  530. uploadTime: mockFile.uploadTime.toISOString()
  531. });
  532. });
  533. it('should search files successfully', async () => {
  534. const mockFiles = [
  535. {
  536. id: 1,
  537. name: 'document.pdf',
  538. type: 'application/pdf',
  539. size: 1024,
  540. path: '/uploads/document.pdf',
  541. fullUrl: 'https://minio.example.com/d8dai/uploads/document.pdf',
  542. description: null,
  543. uploadUserId: 1,
  544. uploadUser: user1Response,
  545. uploadTime: new Date(),
  546. lastUpdated: null,
  547. createdAt: new Date(),
  548. updatedAt: new Date()
  549. }
  550. ];
  551. // 设置ConcreteCrudService的mock返回数据
  552. vi.mocked(ConcreteCrudService).mockImplementation(() => ({
  553. getList: vi.fn().mockResolvedValue([mockFiles, mockFiles.length])
  554. } as unknown as ConcreteCrudService<any>));
  555. const response = await client.files.$get({
  556. query: { keyword: 'document' }
  557. },
  558. {
  559. headers: {
  560. 'Authorization': 'Bearer test-token'
  561. }
  562. });
  563. expect(response.status).toBe(200);
  564. const result = await response.json();
  565. expect(result).toEqual({
  566. data: mockFiles.map(file => ({
  567. ...file,
  568. createdAt: file.createdAt.toISOString(),
  569. updatedAt: file.updatedAt.toISOString(),
  570. uploadTime: file.uploadTime.toISOString()
  571. })),
  572. pagination: {
  573. current: 1,
  574. pageSize: 10,
  575. total: mockFiles.length
  576. }
  577. });
  578. expect(vi.mocked(ConcreteCrudService).mock.results[0].value.getList).toHaveBeenCalledWith(1, 10, 'document', ['name', 'type', 'description'], undefined, ['uploadUser'], { id: 'DESC' }, undefined);
  579. });
  580. });
  581. });