files.integration.test.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. import { describe, it, expect, beforeEach, vi } from 'vitest';
  2. import { testClient } from 'hono/testing';
  3. import {
  4. IntegrationTestDatabase,
  5. setupIntegrationDatabaseHooks,
  6. TestDataFactory
  7. } from '../utils/integration-test-db';
  8. import { IntegrationTestAssertions } from '../utils/integration-test-utils';
  9. import { fileApiRoutes } from '../../src/api';
  10. import { AuthService } from '@d8d/auth-module';
  11. import { UserService } from '@d8d/user-module';
  12. import { MinioService } from '@d8d/file-module';
  13. // Mock MinIO service to avoid real connections in tests
  14. vi.mock('@d8d/file-module', async (importOriginal) => {
  15. const actual = await importOriginal();
  16. return {
  17. ...actual,
  18. MinioService: vi.fn(() => ({
  19. bucketName: 'd8dai',
  20. ensureBucketExists: vi.fn().mockResolvedValue(true),
  21. objectExists: vi.fn().mockImplementation((bucket, key) => {
  22. // 对于删除操作,假设文件存在
  23. if (key.includes('testfile_delete') || key.includes('testfile_url') || key.includes('testfile_download')) {
  24. return Promise.resolve(true);
  25. }
  26. // 其他情况假设文件不存在
  27. return Promise.resolve(false);
  28. }),
  29. deleteObject: vi.fn().mockResolvedValue(true),
  30. generateUploadPolicy: vi.fn().mockResolvedValue({
  31. 'x-amz-algorithm': 'AWS4-HMAC-SHA256',
  32. 'x-amz-credential': 'test-credential',
  33. 'x-amz-date': '20250101T120000Z',
  34. policy: 'test-policy',
  35. 'x-amz-signature': 'test-signature',
  36. host: 'https://minio.example.com',
  37. key: 'test-key',
  38. bucket: 'd8dai'
  39. }),
  40. getPresignedFileUrl: vi.fn().mockResolvedValue('https://minio.example.com/presigned-url'),
  41. getPresignedFileDownloadUrl: vi.fn().mockResolvedValue('https://minio.example.com/download-url'),
  42. createMultipartUpload: vi.fn().mockResolvedValue('test-upload-id'),
  43. generateMultipartUploadUrls: vi.fn().mockResolvedValue(['https://minio.example.com/part1', 'https://minio.example.com/part2']),
  44. completeMultipartUpload: vi.fn().mockResolvedValue({
  45. size: 104857600
  46. }),
  47. createObject: vi.fn().mockResolvedValue('https://minio.example.com/d8dai/test-file'),
  48. getFileUrl: vi.fn().mockReturnValue('https://minio.example.com/d8dai/test-file')
  49. }))
  50. };
  51. });
  52. // 设置集成测试钩子
  53. setupIntegrationDatabaseHooks()
  54. describe('文件API集成测试 (使用hono/testing)', () => {
  55. let client: ReturnType<typeof testClient<typeof fileApiRoutes>>['api']['v1'];
  56. let testToken: string;
  57. beforeEach(async () => {
  58. // 创建测试客户端
  59. client = testClient(fileApiRoutes).api.v1;
  60. // 创建测试用户并生成token
  61. const dataSource = await IntegrationTestDatabase.getDataSource();
  62. const userService = new UserService(dataSource);
  63. const authService = new AuthService(userService);
  64. // 确保admin用户存在
  65. const user = await authService.ensureAdminExists();
  66. // 生成admin用户的token
  67. testToken = authService.generateToken(user);
  68. });
  69. describe('文件上传策略测试', () => {
  70. it('应该成功生成文件上传策略', async () => {
  71. const fileData = {
  72. name: 'test.txt',
  73. type: 'text/plain',
  74. size: 1024,
  75. path: '/uploads/test.txt',
  76. description: 'Test file'
  77. };
  78. const response = await client.files['upload-policy'].$post({
  79. json: fileData
  80. },
  81. {
  82. headers: {
  83. 'Authorization': `Bearer ${testToken}`
  84. }
  85. });
  86. // 断言响应
  87. if (response.status !== 200) {
  88. const errorData = await response.json();
  89. console.debug('File upload policy error:', JSON.stringify(errorData, null, 2));
  90. }
  91. expect(response.status).toBe(200);
  92. if (response.status === 200) {
  93. const responseData = await response.json();
  94. expect(responseData).toHaveProperty('file');
  95. expect(responseData).toHaveProperty('uploadPolicy');
  96. expect(responseData.file.name).toBe(fileData.name);
  97. expect(responseData.file.type).toBe(fileData.type);
  98. expect(responseData.file.size).toBe(fileData.size);
  99. }
  100. });
  101. it('应该拒绝无效请求数据的文件上传策略', async () => {
  102. const invalidData = {
  103. name: '', // 空文件名
  104. type: 'text/plain'
  105. };
  106. const response = await client.files['upload-policy'].$post({
  107. json: invalidData as any
  108. },
  109. {
  110. headers: {
  111. 'Authorization': `Bearer ${testToken}`
  112. }
  113. });
  114. expect(response.status).toBe(400);
  115. });
  116. it('应该拒绝无认证令牌的文件上传策略请求', async () => {
  117. const fileData = {
  118. name: 'test.txt',
  119. type: 'text/plain',
  120. size: 1024,
  121. path: '/uploads/test.txt'
  122. };
  123. const response = await client.files['upload-policy'].$post({
  124. json: fileData
  125. });
  126. expect(response.status).toBe(401);
  127. });
  128. });
  129. describe('文件URL生成测试', () => {
  130. it('应该成功生成文件访问URL', async () => {
  131. const dataSource = await IntegrationTestDatabase.getDataSource();
  132. if (!dataSource) throw new Error('Database not initialized');
  133. // 创建测试文件
  134. const testFile = await TestDataFactory.createTestFile(dataSource, {
  135. name: 'testfile_url.txt',
  136. type: 'text/plain',
  137. size: 1024,
  138. path: 'testfile_url.txt'
  139. });
  140. const response = await client.files[':id']['url'].$get({
  141. param: { id: testFile.id }
  142. },
  143. {
  144. headers: {
  145. 'Authorization': `Bearer ${testToken}`
  146. }
  147. });
  148. expect(response.status).toBe(200);
  149. if (response.status === 200) {
  150. const responseData = await response.json();
  151. expect(responseData).toHaveProperty('url');
  152. expect(typeof responseData.url).toBe('string');
  153. }
  154. });
  155. it('应该返回404当文件不存在时', async () => {
  156. const response = await client.files[':id']['url'].$get({
  157. param: { id: 999999 }
  158. },
  159. {
  160. headers: {
  161. 'Authorization': `Bearer ${testToken}`
  162. }
  163. });
  164. expect(response.status).toBe(404);
  165. });
  166. });
  167. describe('文件下载URL生成测试', () => {
  168. it('应该成功生成文件下载URL', async () => {
  169. const dataSource = await IntegrationTestDatabase.getDataSource();
  170. if (!dataSource) throw new Error('Database not initialized');
  171. // 创建测试文件
  172. const testFile = await TestDataFactory.createTestFile(dataSource, {
  173. name: 'testfile_download.txt',
  174. type: 'text/plain',
  175. size: 1024,
  176. path: 'testfile_download.txt'
  177. });
  178. const response = await client.files[':id']['download'].$get({
  179. param: { id: testFile.id }
  180. },
  181. {
  182. headers: {
  183. 'Authorization': `Bearer ${testToken}`
  184. }
  185. });
  186. expect(response.status).toBe(200);
  187. if (response.status === 200) {
  188. const responseData = await response.json();
  189. expect(responseData).toHaveProperty('url');
  190. expect(responseData).toHaveProperty('filename');
  191. expect(responseData.filename).toBe('testfile_download.txt');
  192. }
  193. });
  194. it('应该返回404当文件不存在时', async () => {
  195. const response = await client.files[':id']['download'].$get({
  196. param: { id: 999999 }
  197. },
  198. {
  199. headers: {
  200. 'Authorization': `Bearer ${testToken}`
  201. }
  202. });
  203. expect(response.status).toBe(404);
  204. });
  205. });
  206. describe('文件删除测试', () => {
  207. it.skip('应该成功删除文件', async () => {
  208. const dataSource = await IntegrationTestDatabase.getDataSource();
  209. if (!dataSource) throw new Error('Database not initialized');
  210. // 创建测试文件
  211. const testFile = await TestDataFactory.createTestFile(dataSource, {
  212. name: 'testfile_delete.txt',
  213. type: 'text/plain',
  214. size: 1024,
  215. path: 'testfile_delete.txt'
  216. });
  217. console.debug('Created test file for deletion:', {
  218. id: testFile.id,
  219. name: testFile.name,
  220. path: testFile.path
  221. });
  222. const response = await client.files[':id'].$delete({
  223. param: { id: testFile.id }
  224. },
  225. {
  226. headers: {
  227. 'Authorization': `Bearer ${testToken}`
  228. }
  229. });
  230. if (response.status !== 200) {
  231. const errorData = await response.json();
  232. console.debug('File deletion error:', JSON.stringify(errorData, null, 2));
  233. }
  234. IntegrationTestAssertions.expectStatus(response, 200);
  235. // 验证文件已从数据库中删除
  236. const fileRepository = dataSource.getRepository('File');
  237. const deletedFile = await fileRepository.findOne({
  238. where: { id: testFile.id }
  239. });
  240. expect(deletedFile).toBeNull();
  241. });
  242. it('应该返回404当删除不存在的文件时', async () => {
  243. const response = await client.files[':id'].$delete({
  244. param: { id: 999999 }
  245. },
  246. {
  247. headers: {
  248. 'Authorization': `Bearer ${testToken}`
  249. }
  250. });
  251. IntegrationTestAssertions.expectStatus(response, 404);
  252. });
  253. });
  254. describe('文件CRUD操作测试', () => {
  255. it('应该成功获取文件列表', async () => {
  256. const dataSource = await IntegrationTestDatabase.getDataSource();
  257. if (!dataSource) throw new Error('Database not initialized');
  258. // 创建几个测试文件
  259. await TestDataFactory.createTestFile(dataSource, {
  260. name: 'file1.txt',
  261. type: 'text/plain',
  262. size: 1024,
  263. path: 'file1.txt'
  264. });
  265. await TestDataFactory.createTestFile(dataSource, {
  266. name: 'file2.txt',
  267. type: 'text/plain',
  268. size: 2048,
  269. path: 'file2.txt'
  270. });
  271. const response = await client.files.$get({
  272. query: {}
  273. },
  274. {
  275. headers: {
  276. 'Authorization': `Bearer ${testToken}`
  277. }
  278. });
  279. expect(response.status).toBe(200);
  280. if (response.status === 200) {
  281. const responseData = await response.json();
  282. expect(Array.isArray(responseData.data)).toBe(true);
  283. expect(responseData.data.length).toBeGreaterThanOrEqual(2);
  284. }
  285. });
  286. it('应该成功获取单个文件详情', async () => {
  287. const dataSource = await IntegrationTestDatabase.getDataSource();
  288. if (!dataSource) throw new Error('Database not initialized');
  289. const testFile = await TestDataFactory.createTestFile(dataSource, {
  290. name: 'testfile_detail.txt',
  291. type: 'text/plain',
  292. size: 1024,
  293. path: 'testfile_detail.txt'
  294. });
  295. const response = await client.files[':id'].$get({
  296. param: { id: testFile.id }
  297. },
  298. {
  299. headers: {
  300. 'Authorization': `Bearer ${testToken}`
  301. }
  302. });
  303. expect(response.status).toBe(200);
  304. if (response.status === 200) {
  305. const responseData = await response.json();
  306. expect(responseData.id).toBe(testFile.id);
  307. expect(responseData.name).toBe(testFile.name);
  308. expect(responseData.type).toBe(testFile.type);
  309. }
  310. });
  311. it('应该能够按文件名搜索文件', async () => {
  312. const dataSource = await IntegrationTestDatabase.getDataSource();
  313. if (!dataSource) throw new Error('Database not initialized');
  314. await TestDataFactory.createTestFile(dataSource, {
  315. name: 'search_file_1.txt',
  316. type: 'text/plain',
  317. size: 1024,
  318. path: 'search_file_1.txt'
  319. });
  320. await TestDataFactory.createTestFile(dataSource, {
  321. name: 'search_file_2.txt',
  322. type: 'text/plain',
  323. size: 2048,
  324. path: 'search_file_2.txt'
  325. });
  326. await TestDataFactory.createTestFile(dataSource, {
  327. name: 'other_file.txt',
  328. type: 'text/plain',
  329. size: 1024,
  330. path: 'other_file.txt'
  331. });
  332. const response = await client.files.$get({
  333. query: { keyword: 'search_file' }
  334. },
  335. {
  336. headers: {
  337. 'Authorization': `Bearer ${testToken}`
  338. }
  339. });
  340. IntegrationTestAssertions.expectStatus(response, 200);
  341. if (response.status === 200) {
  342. const responseData = await response.json();
  343. expect(Array.isArray(responseData.data)).toBe(true);
  344. expect(responseData.data.length).toBe(2);
  345. // 验证搜索结果包含正确的文件
  346. const filenames = responseData.data.map((file: any) => file.name);
  347. expect(filenames).toContain('search_file_1.txt');
  348. expect(filenames).toContain('search_file_2.txt');
  349. expect(filenames).not.toContain('other_file.txt');
  350. }
  351. });
  352. });
  353. describe('多部分上传测试', () => {
  354. it('应该成功生成多部分上传策略', async () => {
  355. const multipartData = {
  356. fileKey: 'large-file.zip',
  357. totalSize: 1024 * 1024 * 100, // 100MB
  358. partSize: 1024 * 1024 * 20, // 20MB
  359. name: 'large-file.zip',
  360. type: 'application/zip'
  361. };
  362. const response = await client.files['multipart-policy'].$post({
  363. json: multipartData
  364. },
  365. {
  366. headers: {
  367. 'Authorization': `Bearer ${testToken}`
  368. }
  369. });
  370. expect(response.status).toBe(200);
  371. if (response.status === 200) {
  372. const responseData = await response.json();
  373. expect(responseData).toHaveProperty('uploadId');
  374. expect(responseData).toHaveProperty('bucket');
  375. expect(responseData).toHaveProperty('key');
  376. expect(responseData).toHaveProperty('partUrls');
  377. }
  378. });
  379. it('应该拒绝无效的多部分上传请求数据', async () => {
  380. const invalidData = {
  381. name: 'test.zip'
  382. // 缺少必需字段: fileKey, totalSize, partSize
  383. };
  384. const response = await client.files['multipart-policy'].$post({
  385. json: invalidData as any
  386. },
  387. {
  388. headers: {
  389. 'Authorization': `Bearer ${testToken}`
  390. }
  391. });
  392. expect(response.status).toBe(400);
  393. });
  394. it.skip('应该成功完成多部分上传', async () => {
  395. const dataSource = await IntegrationTestDatabase.getDataSource();
  396. if (!dataSource) throw new Error('Database not initialized');
  397. // 先创建一个文件记录 - 确保path与key完全匹配
  398. const testFile = await TestDataFactory.createTestFile(dataSource, {
  399. name: 'test-multipart-file.zip',
  400. type: 'application/zip',
  401. size: 104857600,
  402. path: '1/test-file.zip'
  403. });
  404. console.debug('Created test file for multipart completion:', {
  405. id: testFile.id,
  406. name: testFile.name,
  407. path: testFile.path
  408. });
  409. const completeData = {
  410. uploadId: 'upload-123',
  411. bucket: 'd8dai',
  412. key: '1/test-file.zip',
  413. parts: [
  414. { partNumber: 1, etag: 'etag1' },
  415. { partNumber: 2, etag: 'etag2' }
  416. ]
  417. };
  418. const response = await client.files['multipart-complete'].$post({
  419. json: completeData
  420. },
  421. {
  422. headers: {
  423. 'Authorization': `Bearer ${testToken}`
  424. }
  425. });
  426. if (response.status !== 200) {
  427. const errorData = await response.json();
  428. console.debug('Multipart completion error:', JSON.stringify(errorData, null, 2));
  429. }
  430. expect(response.status).toBe(200);
  431. if (response.status === 200) {
  432. const responseData = await response.json();
  433. expect(responseData).toHaveProperty('fileId');
  434. expect(responseData).toHaveProperty('url');
  435. expect(responseData).toHaveProperty('key');
  436. }
  437. });
  438. it('应该拒绝无效的完成多部分上传请求数据', async () => {
  439. const invalidData = {
  440. uploadId: 'upload-123'
  441. // 缺少必需字段: bucket, key, parts
  442. };
  443. const response = await client.files['multipart-complete'].$post({
  444. json: invalidData as any
  445. },
  446. {
  447. headers: {
  448. 'Authorization': `Bearer ${testToken}`
  449. }
  450. });
  451. expect(response.status).toBe(400);
  452. });
  453. });
  454. });