file.service.test.ts 11 KB

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