file.service.test.ts 14 KB

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