file.service.test.ts 13 KB

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