file.service.test.ts 14 KB

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