files.integration.test.ts 14 KB

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