files.integration.test.ts 20 KB

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