file.service.test.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  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, 'getById').mockResolvedValue(mockFile);
  103. vi.spyOn(fileService, 'delete').mockResolvedValue(true);
  104. const result = await fileService.deleteFile(1);
  105. expect(fileService.getById).toHaveBeenCalledWith(1);
  106. expect(mockObjectExists).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
  107. expect(mockDeleteObject).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
  108. expect(fileService.delete).toHaveBeenCalledWith(1);
  109. expect(result).toBe(true);
  110. });
  111. it('should delete database record even when MinIO file not found', async () => {
  112. const mockFile = {
  113. id: 1,
  114. path: '1/test-file.txt',
  115. name: 'test-file.txt'
  116. } as File;
  117. const mockObjectExists = vi.fn().mockResolvedValue(false);
  118. vi.mocked(MinioService).mockImplementation(() => ({
  119. objectExists: mockObjectExists,
  120. deleteObject: vi.fn(),
  121. bucketName: 'd8dai'
  122. } as unknown as MinioService));
  123. const fileService = new FileService(mockDataSource);
  124. vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);
  125. vi.spyOn(fileService, 'delete').mockResolvedValue(true);
  126. const result = await fileService.deleteFile(1);
  127. expect(mockObjectExists).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
  128. expect(fileService.delete).toHaveBeenCalledWith(1);
  129. expect(result).toBe(true);
  130. expect(logger.error).toHaveBeenCalled();
  131. });
  132. it('should throw error when file not found', async () => {
  133. const fileService = new FileService(mockDataSource);
  134. vi.spyOn(fileService, 'getById').mockResolvedValue(null);
  135. await expect(fileService.deleteFile(999)).rejects.toThrow('文件不存在');
  136. });
  137. });
  138. describe('getFileUrl', () => {
  139. it('should return file URL successfully', async () => {
  140. const mockFile = {
  141. id: 1,
  142. path: '1/test-file.txt'
  143. } as File;
  144. const mockPresignedUrl = 'https://minio.example.com/presigned-url';
  145. const mockGetPresignedFileUrl = vi.fn().mockResolvedValue(mockPresignedUrl);
  146. vi.mocked(MinioService).mockImplementation(() => ({
  147. getPresignedFileUrl: mockGetPresignedFileUrl,
  148. bucketName: 'd8dai'
  149. } as unknown as MinioService));
  150. const fileService = new FileService(mockDataSource);
  151. vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);
  152. const result = await fileService.getFileUrl(1);
  153. expect(fileService.getById).toHaveBeenCalledWith(1);
  154. expect(mockGetPresignedFileUrl).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
  155. expect(result).toBe(mockPresignedUrl);
  156. });
  157. it('should throw error when file not found', async () => {
  158. const fileService = new FileService(mockDataSource);
  159. vi.spyOn(fileService, 'getById').mockResolvedValue(null);
  160. await expect(fileService.getFileUrl(999)).rejects.toThrow('文件不存在');
  161. });
  162. });
  163. describe('getFileDownloadUrl', () => {
  164. it('should return download URL with filename', async () => {
  165. const mockFile = {
  166. id: 1,
  167. path: '1/test-file.txt',
  168. name: '测试文件.txt'
  169. } as File;
  170. const mockPresignedUrl = 'https://minio.example.com/download-url';
  171. const mockGetPresignedFileDownloadUrl = vi.fn().mockResolvedValue(mockPresignedUrl);
  172. vi.mocked(MinioService).mockImplementation(() => ({
  173. getPresignedFileDownloadUrl: mockGetPresignedFileDownloadUrl,
  174. bucketName: 'd8dai'
  175. } as unknown as MinioService));
  176. const fileService = new FileService(mockDataSource);
  177. vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);
  178. const result = await fileService.getFileDownloadUrl(1);
  179. expect(fileService.getById).toHaveBeenCalledWith(1);
  180. expect(mockGetPresignedFileDownloadUrl).toHaveBeenCalledWith(
  181. 'd8dai',
  182. '1/test-file.txt',
  183. '测试文件.txt'
  184. );
  185. expect(result).toEqual({
  186. url: mockPresignedUrl,
  187. filename: '测试文件.txt'
  188. });
  189. });
  190. it('should throw error when file not found', async () => {
  191. const fileService = new FileService(mockDataSource);
  192. vi.spyOn(fileService, 'getById').mockResolvedValue(null);
  193. await expect(fileService.getFileDownloadUrl(999)).rejects.toThrow('文件不存在');
  194. });
  195. });
  196. describe('createMultipartUploadPolicy', () => {
  197. it('should create multipart upload policy successfully', async () => {
  198. const mockFileData = {
  199. name: 'large-file.zip',
  200. type: 'application/zip',
  201. uploadUserId: 1
  202. };
  203. const mockUploadId = 'upload-123';
  204. const mockUploadUrls = ['url1', 'url2', 'url3'];
  205. const mockSavedFile = {
  206. id: 1,
  207. ...mockFileData,
  208. path: '1/test-uuid-123-large-file.zip',
  209. uploadTime: new Date(),
  210. createdAt: new Date(),
  211. updatedAt: new Date()
  212. } as File;
  213. const mockCreateMultipartUpload = vi.fn().mockResolvedValue(mockUploadId);
  214. const mockGenerateMultipartUploadUrls = vi.fn().mockResolvedValue(mockUploadUrls);
  215. vi.mocked(MinioService).mockImplementation(() => ({
  216. createMultipartUpload: mockCreateMultipartUpload,
  217. generateMultipartUploadUrls: mockGenerateMultipartUploadUrls,
  218. bucketName: 'd8dai'
  219. } as unknown as MinioService));
  220. const fileService = new FileService(mockDataSource);
  221. vi.spyOn(fileService, 'create').mockResolvedValue(mockSavedFile);
  222. const result = await fileService.createMultipartUploadPolicy(mockFileData, 3);
  223. expect(mockCreateMultipartUpload).toHaveBeenCalledWith('d8dai', '1/test-uuid-123-large-file.zip');
  224. expect(mockGenerateMultipartUploadUrls).toHaveBeenCalledWith(
  225. 'd8dai',
  226. '1/test-uuid-123-large-file.zip',
  227. mockUploadId,
  228. 3
  229. );
  230. expect(result).toEqual({
  231. file: mockSavedFile,
  232. uploadId: mockUploadId,
  233. uploadUrls: mockUploadUrls,
  234. bucket: 'd8dai',
  235. key: '1/test-uuid-123-large-file.zip'
  236. });
  237. });
  238. it('should handle errors during multipart upload creation', async () => {
  239. const mockFileData = {
  240. name: 'large-file.zip',
  241. uploadUserId: 1
  242. };
  243. const mockCreateMultipartUpload = vi.fn().mockRejectedValue(new Error('MinIO error'));
  244. vi.mocked(MinioService).mockImplementation(() => ({
  245. createMultipartUpload: mockCreateMultipartUpload,
  246. bucketName: 'd8dai'
  247. } as unknown as MinioService));
  248. const fileService = new FileService(mockDataSource);
  249. await expect(fileService.createMultipartUploadPolicy(mockFileData, 3)).rejects.toThrow('创建多部分上传策略失败');
  250. expect(logger.error).toHaveBeenCalled();
  251. });
  252. });
  253. describe('completeMultipartUpload', () => {
  254. it('should complete multipart upload successfully', async () => {
  255. const uploadData = {
  256. uploadId: 'upload-123',
  257. bucket: 'd8dai',
  258. key: '1/test-file.txt',
  259. parts: [
  260. { partNumber: 1, etag: 'etag1' },
  261. { partNumber: 2, etag: 'etag2' }
  262. ]
  263. };
  264. const mockFile = {
  265. id: 1,
  266. path: '1/test-file.txt',
  267. size: 0,
  268. updatedAt: new Date()
  269. } as File;
  270. const mockCompleteResult = { size: 2048 };
  271. const mockFileUrl = 'https://minio.example.com/file.txt';
  272. const mockCompleteMultipartUpload = vi.fn().mockResolvedValue(mockCompleteResult);
  273. const mockGetFileUrl = vi.fn().mockReturnValue(mockFileUrl);
  274. vi.mocked(MinioService).mockImplementation(() => ({
  275. completeMultipartUpload: mockCompleteMultipartUpload,
  276. getFileUrl: mockGetFileUrl
  277. } as unknown as MinioService));
  278. const mockRepository = {
  279. findOneBy: vi.fn().mockResolvedValue(mockFile),
  280. save: vi.fn().mockResolvedValue({ ...mockFile, size: 2048 } as File)
  281. };
  282. mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository);
  283. const fileService = new FileService(mockDataSource);
  284. const result = await fileService.completeMultipartUpload(uploadData);
  285. expect(mockCompleteMultipartUpload).toHaveBeenCalledWith(
  286. 'd8dai',
  287. '1/test-file.txt',
  288. 'upload-123',
  289. [{ PartNumber: 1, ETag: 'etag1' }, { PartNumber: 2, ETag: 'etag2' }]
  290. );
  291. expect(mockRepository.findOneBy).toHaveBeenCalledWith({ path: '1/test-file.txt' });
  292. expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining({
  293. size: 2048
  294. }));
  295. expect(result).toEqual({
  296. fileId: 1,
  297. url: mockFileUrl,
  298. key: '1/test-file.txt',
  299. size: 2048
  300. });
  301. });
  302. it('should throw error when file record not found', async () => {
  303. const uploadData = {
  304. uploadId: 'upload-123',
  305. bucket: 'd8dai',
  306. key: '1/nonexistent.txt',
  307. parts: [{ partNumber: 1, etag: 'etag1' }]
  308. };
  309. const mockCompleteMultipartUpload = vi.fn().mockResolvedValue({ size: 1024 });
  310. vi.mocked(MinioService).mockImplementation(() => ({
  311. completeMultipartUpload: mockCompleteMultipartUpload
  312. } as unknown as MinioService));
  313. const mockRepository = {
  314. findOneBy: vi.fn().mockResolvedValue(null)
  315. };
  316. mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository);
  317. const fileService = new FileService(mockDataSource);
  318. await expect(fileService.completeMultipartUpload(uploadData)).rejects.toThrow('文件记录不存在');
  319. });
  320. it('should handle errors during completion', async () => {
  321. const uploadData = {
  322. uploadId: 'upload-123',
  323. bucket: 'd8dai',
  324. key: '1/test-file.txt',
  325. parts: [{ partNumber: 1, etag: 'etag1' }]
  326. };
  327. const mockFile = {
  328. id: 1,
  329. path: '1/test-file.txt',
  330. size: 0,
  331. updatedAt: new Date()
  332. } as File;
  333. const mockRepository = {
  334. findOneBy: vi.fn().mockResolvedValue(mockFile),
  335. save: vi.fn()
  336. };
  337. const mockCompleteMultipartUpload = vi.fn().mockRejectedValue(new Error('Completion failed'));
  338. mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository);
  339. vi.mocked(MinioService).mockImplementation(() => ({
  340. completeMultipartUpload: mockCompleteMultipartUpload
  341. } as unknown as MinioService));
  342. const fileService = new FileService(mockDataSource);
  343. await expect(fileService.completeMultipartUpload(uploadData)).rejects.toThrow('完成分片上传失败');
  344. expect(logger.error).toHaveBeenCalled();
  345. });
  346. });
  347. });