file.service.test.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
  2. import { DataSource } from 'typeorm';
  3. import { FileService } from '../../src/services/file.service';
  4. import { File } from '../../src/entities/file.entity';
  5. import { MinioService } from '../../src/services/minio.service';
  6. import { logger } from '@d8d/shared-utils';
  7. // Mock dependencies
  8. vi.mock('../../src/services/minio.service');
  9. vi.mock('@d8d/shared-utils', () => ({
  10. logger: {
  11. error: vi.fn(),
  12. db: vi.fn()
  13. },
  14. ErrorSchema: {}
  15. }));
  16. vi.mock('uuid', () => ({
  17. v4: () => 'test-uuid-123'
  18. }));
  19. describe('FileService', () => {
  20. let mockDataSource: DataSource;
  21. beforeEach(() => {
  22. mockDataSource = {
  23. getRepository: vi.fn(() => ({
  24. findOne: vi.fn(),
  25. findOneBy: vi.fn(),
  26. save: vi.fn()
  27. }))
  28. } as unknown as DataSource;
  29. });
  30. afterEach(() => {
  31. vi.clearAllMocks();
  32. });
  33. describe('createFile', () => {
  34. it('should create file with upload policy successfully', async () => {
  35. const mockFileData = {
  36. name: 'test.txt',
  37. type: 'text/plain',
  38. size: 1024,
  39. uploadUserId: 1
  40. };
  41. const mockUploadPolicy = {
  42. 'x-amz-algorithm': 'test-algorithm',
  43. 'x-amz-credential': 'test-credential',
  44. host: 'https://minio.example.com'
  45. };
  46. const mockSavedFile = {
  47. id: 1,
  48. ...mockFileData,
  49. path: '1/test-uuid-123-test.txt',
  50. uploadTime: new Date(),
  51. createdAt: new Date(),
  52. updatedAt: new Date()
  53. };
  54. const mockGenerateUploadPolicy = vi.fn().mockResolvedValue(mockUploadPolicy);
  55. vi.mocked(MinioService).mockImplementation(() => ({
  56. generateUploadPolicy: mockGenerateUploadPolicy
  57. } as unknown as MinioService));
  58. const fileService = new FileService(mockDataSource);
  59. // Mock GenericCrudService methods
  60. vi.spyOn(fileService, 'create').mockResolvedValue(mockSavedFile as File);
  61. const result = await fileService.createFile(mockFileData);
  62. expect(mockGenerateUploadPolicy).toHaveBeenCalledWith('1/test-uuid-123-test.txt');
  63. expect(fileService.create).toHaveBeenCalledWith(expect.objectContaining({
  64. name: 'test.txt',
  65. path: '1/test-uuid-123-test.txt',
  66. uploadUserId: 1
  67. }));
  68. expect(result).toEqual({
  69. file: mockSavedFile,
  70. uploadPolicy: mockUploadPolicy
  71. });
  72. });
  73. it('should handle errors during file creation', async () => {
  74. const mockFileData = {
  75. name: 'test.txt',
  76. uploadUserId: 1
  77. };
  78. const mockGenerateUploadPolicy = vi.fn().mockRejectedValue(new Error('MinIO error'));
  79. vi.mocked(MinioService).mockImplementation(() => ({
  80. generateUploadPolicy: mockGenerateUploadPolicy
  81. } as unknown as MinioService));
  82. const fileService = new FileService(mockDataSource);
  83. await expect(fileService.createFile(mockFileData)).rejects.toThrow('文件创建失败');
  84. expect(logger.error).toHaveBeenCalled();
  85. });
  86. });
  87. describe('deleteFile', () => {
  88. it('should delete file successfully when file exists', async () => {
  89. const mockFile = {
  90. id: 1,
  91. path: '1/test-file.txt',
  92. name: 'test-file.txt'
  93. } as File;
  94. const mockObjectExists = vi.fn().mockResolvedValue(true);
  95. const mockDeleteObject = vi.fn().mockResolvedValue(undefined);
  96. vi.mocked(MinioService).mockImplementation(() => ({
  97. objectExists: mockObjectExists,
  98. deleteObject: mockDeleteObject,
  99. bucketName: 'd8dai'
  100. } as unknown as MinioService));
  101. const fileService = new FileService(mockDataSource);
  102. vi.spyOn(fileService.repository, 'findOne').mockResolvedValue(mockFile);
  103. vi.spyOn(fileService, 'delete').mockResolvedValue(true);
  104. const result = await fileService.deleteFile(1);
  105. expect(fileService.repository.findOne).toHaveBeenCalledWith({
  106. where: { id: 1 }
  107. });
  108. expect(mockObjectExists).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
  109. expect(mockDeleteObject).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
  110. expect(fileService.delete).toHaveBeenCalledWith(1);
  111. expect(result).toBe(true);
  112. });
  113. it('should delete database record even when MinIO file not found', async () => {
  114. const mockFile = {
  115. id: 1,
  116. path: '1/test-file.txt',
  117. name: 'test-file.txt'
  118. } as File;
  119. const mockObjectExists = vi.fn().mockResolvedValue(false);
  120. vi.mocked(MinioService).mockImplementation(() => ({
  121. objectExists: mockObjectExists,
  122. deleteObject: vi.fn(),
  123. bucketName: 'd8dai'
  124. } as unknown as MinioService));
  125. const fileService = new FileService(mockDataSource);
  126. vi.spyOn(fileService.repository, 'findOne').mockResolvedValue(mockFile);
  127. vi.spyOn(fileService, 'delete').mockResolvedValue(true);
  128. const result = await fileService.deleteFile(1);
  129. expect(fileService.repository.findOne).toHaveBeenCalledWith({
  130. where: { id: 1 }
  131. });
  132. expect(mockObjectExists).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
  133. expect(fileService.delete).toHaveBeenCalledWith(1);
  134. expect(result).toBe(true);
  135. expect(logger.error).toHaveBeenCalled();
  136. });
  137. it('should throw error when file not found', async () => {
  138. const fileService = new FileService(mockDataSource);
  139. vi.spyOn(fileService.repository, 'findOne').mockResolvedValue(null);
  140. await expect(fileService.deleteFile(999)).rejects.toThrow('文件不存在');
  141. });
  142. });
  143. describe('getFileUrl', () => {
  144. it('should return file URL successfully', async () => {
  145. const mockFile = {
  146. id: 1,
  147. path: '1/test-file.txt'
  148. } as File;
  149. const mockPresignedUrl = 'https://minio.example.com/presigned-url';
  150. const mockGetPresignedFileUrl = vi.fn().mockResolvedValue(mockPresignedUrl);
  151. vi.mocked(MinioService).mockImplementation(() => ({
  152. getPresignedFileUrl: mockGetPresignedFileUrl,
  153. bucketName: 'd8dai'
  154. } as unknown as MinioService));
  155. const fileService = new FileService(mockDataSource);
  156. vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);
  157. const result = await fileService.getFileUrl(1);
  158. expect(fileService.getById).toHaveBeenCalledWith(1);
  159. expect(mockGetPresignedFileUrl).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
  160. expect(result).toBe(mockPresignedUrl);
  161. });
  162. it('should throw error when file not found', async () => {
  163. const fileService = new FileService(mockDataSource);
  164. vi.spyOn(fileService, 'getById').mockResolvedValue(null);
  165. await expect(fileService.getFileUrl(999)).rejects.toThrow('文件不存在');
  166. });
  167. });
  168. describe('getFileDownloadUrl', () => {
  169. it('should return download URL with filename', async () => {
  170. const mockFile = {
  171. id: 1,
  172. path: '1/test-file.txt',
  173. name: '测试文件.txt'
  174. } as File;
  175. const mockPresignedUrl = 'https://minio.example.com/download-url';
  176. const mockGetPresignedFileDownloadUrl = vi.fn().mockResolvedValue(mockPresignedUrl);
  177. vi.mocked(MinioService).mockImplementation(() => ({
  178. getPresignedFileDownloadUrl: mockGetPresignedFileDownloadUrl,
  179. bucketName: 'd8dai'
  180. } as unknown as MinioService));
  181. const fileService = new FileService(mockDataSource);
  182. vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);
  183. const result = await fileService.getFileDownloadUrl(1);
  184. expect(fileService.getById).toHaveBeenCalledWith(1);
  185. expect(mockGetPresignedFileDownloadUrl).toHaveBeenCalledWith(
  186. 'd8dai',
  187. '1/test-file.txt',
  188. '测试文件.txt'
  189. );
  190. expect(result).toEqual({
  191. url: mockPresignedUrl,
  192. filename: '测试文件.txt'
  193. });
  194. });
  195. it('should throw error when file not found', async () => {
  196. const fileService = new FileService(mockDataSource);
  197. vi.spyOn(fileService, 'getById').mockResolvedValue(null);
  198. await expect(fileService.getFileDownloadUrl(999)).rejects.toThrow('文件不存在');
  199. });
  200. });
  201. describe('createMultipartUploadPolicy', () => {
  202. it('should create multipart upload policy successfully', async () => {
  203. const mockFileData = {
  204. name: 'large-file.zip',
  205. type: 'application/zip',
  206. uploadUserId: 1
  207. };
  208. const mockUploadId = 'upload-123';
  209. const mockUploadUrls = ['url1', 'url2', 'url3'];
  210. const mockSavedFile = {
  211. id: 1,
  212. ...mockFileData,
  213. path: '1/test-uuid-123-large-file.zip',
  214. uploadTime: new Date(),
  215. createdAt: new Date(),
  216. updatedAt: new Date()
  217. } as File;
  218. const mockCreateMultipartUpload = vi.fn().mockResolvedValue(mockUploadId);
  219. const mockGenerateMultipartUploadUrls = vi.fn().mockResolvedValue(mockUploadUrls);
  220. vi.mocked(MinioService).mockImplementation(() => ({
  221. createMultipartUpload: mockCreateMultipartUpload,
  222. generateMultipartUploadUrls: mockGenerateMultipartUploadUrls,
  223. bucketName: 'd8dai'
  224. } as unknown as MinioService));
  225. const fileService = new FileService(mockDataSource);
  226. vi.spyOn(fileService, 'create').mockResolvedValue(mockSavedFile);
  227. const result = await fileService.createMultipartUploadPolicy(mockFileData, 3);
  228. expect(mockCreateMultipartUpload).toHaveBeenCalledWith('d8dai', '1/test-uuid-123-large-file.zip');
  229. expect(mockGenerateMultipartUploadUrls).toHaveBeenCalledWith(
  230. 'd8dai',
  231. '1/test-uuid-123-large-file.zip',
  232. mockUploadId,
  233. 3
  234. );
  235. expect(result).toEqual({
  236. file: mockSavedFile,
  237. uploadId: mockUploadId,
  238. uploadUrls: mockUploadUrls,
  239. bucket: 'd8dai',
  240. key: '1/test-uuid-123-large-file.zip'
  241. });
  242. });
  243. it('should handle errors during multipart upload creation', async () => {
  244. const mockFileData = {
  245. name: 'large-file.zip',
  246. uploadUserId: 1
  247. };
  248. const mockCreateMultipartUpload = vi.fn().mockRejectedValue(new Error('MinIO error'));
  249. vi.mocked(MinioService).mockImplementation(() => ({
  250. createMultipartUpload: mockCreateMultipartUpload,
  251. bucketName: 'd8dai'
  252. } as unknown as MinioService));
  253. const fileService = new FileService(mockDataSource);
  254. await expect(fileService.createMultipartUploadPolicy(mockFileData, 3)).rejects.toThrow('创建多部分上传策略失败');
  255. expect(logger.error).toHaveBeenCalled();
  256. });
  257. });
  258. describe('completeMultipartUpload', () => {
  259. it('should complete multipart upload successfully', async () => {
  260. const uploadData = {
  261. uploadId: 'upload-123',
  262. bucket: 'd8dai',
  263. key: '1/test-file.txt',
  264. parts: [
  265. { partNumber: 1, etag: 'etag1' },
  266. { partNumber: 2, etag: 'etag2' }
  267. ]
  268. };
  269. const mockFile = {
  270. id: 1,
  271. path: '1/test-file.txt',
  272. size: 0,
  273. updatedAt: new Date()
  274. } as File;
  275. const mockCompleteResult = { size: 2048 };
  276. const mockFileUrl = 'https://minio.example.com/file.txt';
  277. const mockCompleteMultipartUpload = vi.fn().mockResolvedValue(mockCompleteResult);
  278. const mockGetFileUrl = vi.fn().mockReturnValue(mockFileUrl);
  279. vi.mocked(MinioService).mockImplementation(() => ({
  280. completeMultipartUpload: mockCompleteMultipartUpload,
  281. getFileUrl: mockGetFileUrl
  282. } as unknown as MinioService));
  283. const mockRepository = {
  284. findOneBy: vi.fn().mockResolvedValue(mockFile),
  285. save: vi.fn().mockResolvedValue({ ...mockFile, size: 2048 } as File)
  286. };
  287. mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository);
  288. const fileService = new FileService(mockDataSource);
  289. const result = await fileService.completeMultipartUpload(uploadData);
  290. expect(mockCompleteMultipartUpload).toHaveBeenCalledWith(
  291. 'd8dai',
  292. '1/test-file.txt',
  293. 'upload-123',
  294. [{ PartNumber: 1, ETag: 'etag1' }, { PartNumber: 2, ETag: 'etag2' }]
  295. );
  296. expect(mockRepository.findOneBy).toHaveBeenCalledWith({ path: '1/test-file.txt' });
  297. expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining({
  298. size: 2048
  299. }));
  300. expect(result).toEqual({
  301. fileId: 1,
  302. url: mockFileUrl,
  303. key: '1/test-file.txt',
  304. size: 2048
  305. });
  306. });
  307. it('should throw error when file record not found', async () => {
  308. const uploadData = {
  309. uploadId: 'upload-123',
  310. bucket: 'd8dai',
  311. key: '1/nonexistent.txt',
  312. parts: [{ partNumber: 1, etag: 'etag1' }]
  313. };
  314. const mockCompleteMultipartUpload = vi.fn().mockResolvedValue({ size: 1024 });
  315. vi.mocked(MinioService).mockImplementation(() => ({
  316. completeMultipartUpload: mockCompleteMultipartUpload
  317. } as unknown as MinioService));
  318. const mockRepository = {
  319. findOneBy: vi.fn().mockResolvedValue(null)
  320. };
  321. mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository);
  322. const fileService = new FileService(mockDataSource);
  323. await expect(fileService.completeMultipartUpload(uploadData)).rejects.toThrow('文件记录不存在');
  324. });
  325. it('should handle errors during completion', async () => {
  326. const uploadData = {
  327. uploadId: 'upload-123',
  328. bucket: 'd8dai',
  329. key: '1/test-file.txt',
  330. parts: [{ partNumber: 1, etag: 'etag1' }]
  331. };
  332. const mockFile = {
  333. id: 1,
  334. path: '1/test-file.txt',
  335. size: 0,
  336. updatedAt: new Date()
  337. } as File;
  338. const mockRepository = {
  339. findOneBy: vi.fn().mockResolvedValue(mockFile),
  340. save: vi.fn()
  341. };
  342. const mockCompleteMultipartUpload = vi.fn().mockRejectedValue(new Error('Completion failed'));
  343. mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository);
  344. vi.mocked(MinioService).mockImplementation(() => ({
  345. completeMultipartUpload: mockCompleteMultipartUpload
  346. } as unknown as MinioService));
  347. const fileService = new FileService(mockDataSource);
  348. await expect(fileService.completeMultipartUpload(uploadData)).rejects.toThrow('完成分片上传失败');
  349. expect(logger.error).toHaveBeenCalled();
  350. });
  351. });
  352. });