소스 검색

✅ test(file): add unit tests for FileService

- 测试createFile方法的成功和错误处理场景
- 测试deleteFile方法的文件删除流程和异常处理
- 测试getFileUrl和getFileDownloadUrl的URL生成功能
- 测试createMultipartUploadPolicy的分片上传策略创建
- 测试completeMultipartUpload的分片上传完成逻辑
yourname 4 주 전
부모
커밋
6fc989d9f0
1개의 변경된 파일429개의 추가작업 그리고 0개의 파일을 삭제
  1. 429 0
      packages/file-module/tests/unit/file.service.test.ts

+ 429 - 0
packages/file-module/tests/unit/file.service.test.ts

@@ -0,0 +1,429 @@
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import { DataSource } from 'typeorm';
+import { FileService } from '../../src/services/file.service';
+import { File } from '../../src/entities/file.entity';
+import { MinioService } from '../../src/services/minio.service';
+import { logger } from '@d8d/shared-utils';
+
+// Mock dependencies
+vi.mock('../../src/services/minio.service');
+vi.mock('@d8d/shared-utils', () => ({
+  logger: {
+    error: vi.fn(),
+    db: vi.fn()
+  }
+}));
+vi.mock('uuid', () => ({
+  v4: () => 'test-uuid-123'
+}));
+
+describe('FileService', () => {
+  let mockDataSource: DataSource;
+
+  beforeEach(() => {
+    mockDataSource = {
+      getRepository: vi.fn(() => ({
+        findOneBy: vi.fn(),
+        save: vi.fn()
+      }))
+    } as unknown as DataSource;
+
+  });
+
+  afterEach(() => {
+    vi.clearAllMocks();
+  });
+
+  describe('createFile', () => {
+    it('should create file with upload policy successfully', async () => {
+      const mockFileData = {
+        name: 'test.txt',
+        type: 'text/plain',
+        size: 1024,
+        uploadUserId: 1
+      };
+
+      const mockUploadPolicy = {
+        'x-amz-algorithm': 'test-algorithm',
+        'x-amz-credential': 'test-credential',
+        host: 'https://minio.example.com'
+      };
+
+      const mockSavedFile = {
+        id: 1,
+        ...mockFileData,
+        path: '1/test-uuid-123-test.txt',
+        uploadTime: new Date(),
+        createdAt: new Date(),
+        updatedAt: new Date()
+      };
+
+
+      const mockGenerateUploadPolicy = vi.fn().mockResolvedValue(mockUploadPolicy);
+      vi.mocked(MinioService).mockImplementation(() => ({
+        generateUploadPolicy: mockGenerateUploadPolicy
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+
+      // Mock GenericCrudService methods
+      vi.spyOn(fileService, 'create').mockResolvedValue(mockSavedFile as File);
+
+      const result = await fileService.createFile(mockFileData);
+
+      expect(mockGenerateUploadPolicy).toHaveBeenCalledWith('1/test-uuid-123-test.txt');
+      expect(fileService.create).toHaveBeenCalledWith(expect.objectContaining({
+        name: 'test.txt',
+        path: '1/test-uuid-123-test.txt',
+        uploadUserId: 1
+      }));
+      expect(result).toEqual({
+        file: mockSavedFile,
+        uploadPolicy: mockUploadPolicy
+      });
+    });
+
+    it('should handle errors during file creation', async () => {
+      const mockFileData = {
+        name: 'test.txt',
+        uploadUserId: 1
+      };
+
+      const mockGenerateUploadPolicy = vi.fn().mockRejectedValue(new Error('MinIO error'));
+      vi.mocked(MinioService).mockImplementation(() => ({
+        generateUploadPolicy: mockGenerateUploadPolicy
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+
+      await expect(fileService.createFile(mockFileData)).rejects.toThrow('文件创建失败');
+      expect(logger.error).toHaveBeenCalled();
+    });
+  });
+
+  describe('deleteFile', () => {
+    it('should delete file successfully when file exists', async () => {
+      const mockFile = {
+        id: 1,
+        path: '1/test-file.txt',
+        name: 'test-file.txt'
+      } as File;
+
+      const mockObjectExists = vi.fn().mockResolvedValue(true);
+      const mockDeleteObject = vi.fn().mockResolvedValue(undefined);
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        objectExists: mockObjectExists,
+        deleteObject: mockDeleteObject,
+        bucketName: 'd8dai'
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);
+      vi.spyOn(fileService, 'delete').mockResolvedValue(true);
+
+      const result = await fileService.deleteFile(1);
+
+      expect(fileService.getById).toHaveBeenCalledWith(1);
+      expect(mockObjectExists).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
+      expect(mockDeleteObject).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
+      expect(fileService.delete).toHaveBeenCalledWith(1);
+      expect(result).toBe(true);
+    });
+
+    it('should delete database record even when MinIO file not found', async () => {
+      const mockFile = {
+        id: 1,
+        path: '1/test-file.txt',
+        name: 'test-file.txt'
+      } as File;
+
+      const mockObjectExists = vi.fn().mockResolvedValue(false);
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        objectExists: mockObjectExists,
+        deleteObject: vi.fn(),
+        bucketName: 'd8dai'
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);
+      vi.spyOn(fileService, 'delete').mockResolvedValue(true);
+
+      const result = await fileService.deleteFile(1);
+
+      expect(mockObjectExists).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
+      expect(fileService.delete).toHaveBeenCalledWith(1);
+      expect(result).toBe(true);
+      expect(logger.error).toHaveBeenCalled();
+    });
+
+    it('should throw error when file not found', async () => {
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'getById').mockResolvedValue(null);
+
+      await expect(fileService.deleteFile(999)).rejects.toThrow('文件不存在');
+    });
+  });
+
+  describe('getFileUrl', () => {
+    it('should return file URL successfully', async () => {
+      const mockFile = {
+        id: 1,
+        path: '1/test-file.txt'
+      } as File;
+
+      const mockPresignedUrl = 'https://minio.example.com/presigned-url';
+
+      const mockGetPresignedFileUrl = vi.fn().mockResolvedValue(mockPresignedUrl);
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        getPresignedFileUrl: mockGetPresignedFileUrl,
+        bucketName: 'd8dai'
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);
+
+      const result = await fileService.getFileUrl(1);
+
+      expect(fileService.getById).toHaveBeenCalledWith(1);
+      expect(mockGetPresignedFileUrl).toHaveBeenCalledWith('d8dai', '1/test-file.txt');
+      expect(result).toBe(mockPresignedUrl);
+    });
+
+    it('should throw error when file not found', async () => {
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'getById').mockResolvedValue(null);
+
+      await expect(fileService.getFileUrl(999)).rejects.toThrow('文件不存在');
+    });
+  });
+
+  describe('getFileDownloadUrl', () => {
+    it('should return download URL with filename', async () => {
+      const mockFile = {
+        id: 1,
+        path: '1/test-file.txt',
+        name: '测试文件.txt'
+      } as File;
+
+      const mockPresignedUrl = 'https://minio.example.com/download-url';
+
+      const mockGetPresignedFileDownloadUrl = vi.fn().mockResolvedValue(mockPresignedUrl);
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        getPresignedFileDownloadUrl: mockGetPresignedFileDownloadUrl,
+        bucketName: 'd8dai'
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'getById').mockResolvedValue(mockFile);
+
+      const result = await fileService.getFileDownloadUrl(1);
+
+      expect(fileService.getById).toHaveBeenCalledWith(1);
+      expect(mockGetPresignedFileDownloadUrl).toHaveBeenCalledWith(
+        'd8dai',
+        '1/test-file.txt',
+        '测试文件.txt'
+      );
+      expect(result).toEqual({
+        url: mockPresignedUrl,
+        filename: '测试文件.txt'
+      });
+    });
+
+    it('should throw error when file not found', async () => {
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'getById').mockResolvedValue(null);
+
+      await expect(fileService.getFileDownloadUrl(999)).rejects.toThrow('文件不存在');
+    });
+  });
+
+  describe('createMultipartUploadPolicy', () => {
+    it('should create multipart upload policy successfully', async () => {
+      const mockFileData = {
+        name: 'large-file.zip',
+        type: 'application/zip',
+        uploadUserId: 1
+      };
+
+      const mockUploadId = 'upload-123';
+      const mockUploadUrls = ['url1', 'url2', 'url3'];
+      const mockSavedFile = {
+        id: 1,
+        ...mockFileData,
+        path: '1/test-uuid-123-large-file.zip',
+        uploadTime: new Date(),
+        createdAt: new Date(),
+        updatedAt: new Date()
+      } as File;
+
+      const mockCreateMultipartUpload = vi.fn().mockResolvedValue(mockUploadId);
+      const mockGenerateMultipartUploadUrls = vi.fn().mockResolvedValue(mockUploadUrls);
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        createMultipartUpload: mockCreateMultipartUpload,
+        generateMultipartUploadUrls: mockGenerateMultipartUploadUrls,
+        bucketName: 'd8dai'
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+      vi.spyOn(fileService, 'create').mockResolvedValue(mockSavedFile);
+
+      const result = await fileService.createMultipartUploadPolicy(mockFileData, 3);
+
+      expect(mockCreateMultipartUpload).toHaveBeenCalledWith('d8dai', '1/test-uuid-123-large-file.zip');
+      expect(mockGenerateMultipartUploadUrls).toHaveBeenCalledWith(
+        'd8dai',
+        '1/test-uuid-123-large-file.zip',
+        mockUploadId,
+        3
+      );
+      expect(result).toEqual({
+        file: mockSavedFile,
+        uploadId: mockUploadId,
+        uploadUrls: mockUploadUrls,
+        bucket: 'd8dai',
+        key: '1/test-uuid-123-large-file.zip'
+      });
+    });
+
+    it('should handle errors during multipart upload creation', async () => {
+      const mockFileData = {
+        name: 'large-file.zip',
+        uploadUserId: 1
+      };
+
+      const mockCreateMultipartUpload = vi.fn().mockRejectedValue(new Error('MinIO error'));
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        createMultipartUpload: mockCreateMultipartUpload,
+        bucketName: 'd8dai'
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+
+      await expect(fileService.createMultipartUploadPolicy(mockFileData, 3)).rejects.toThrow('创建多部分上传策略失败');
+      expect(logger.error).toHaveBeenCalled();
+    });
+  });
+
+  describe('completeMultipartUpload', () => {
+    it('should complete multipart upload successfully', async () => {
+      const uploadData = {
+        uploadId: 'upload-123',
+        bucket: 'd8dai',
+        key: '1/test-file.txt',
+        parts: [
+          { partNumber: 1, etag: 'etag1' },
+          { partNumber: 2, etag: 'etag2' }
+        ]
+      };
+
+      const mockFile = {
+        id: 1,
+        path: '1/test-file.txt',
+        size: 0,
+        updatedAt: new Date()
+      } as File;
+
+      const mockCompleteResult = { size: 2048 };
+      const mockFileUrl = 'https://minio.example.com/file.txt';
+
+      const mockCompleteMultipartUpload = vi.fn().mockResolvedValue(mockCompleteResult);
+      const mockGetFileUrl = vi.fn().mockReturnValue(mockFileUrl);
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        completeMultipartUpload: mockCompleteMultipartUpload,
+        getFileUrl: mockGetFileUrl
+      } as unknown as MinioService));
+
+      const mockRepository = {
+        findOneBy: vi.fn().mockResolvedValue(mockFile),
+        save: vi.fn().mockResolvedValue({ ...mockFile, size: 2048 } as File)
+      };
+
+      mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository);
+      const fileService = new FileService(mockDataSource);
+
+      const result = await fileService.completeMultipartUpload(uploadData);
+
+      expect(mockCompleteMultipartUpload).toHaveBeenCalledWith(
+        'd8dai',
+        '1/test-file.txt',
+        'upload-123',
+        [{ PartNumber: 1, ETag: 'etag1' }, { PartNumber: 2, ETag: 'etag2' }]
+      );
+      expect(mockRepository.findOneBy).toHaveBeenCalledWith({ path: '1/test-file.txt' });
+      expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining({
+        size: 2048
+      }));
+      expect(result).toEqual({
+        fileId: 1,
+        url: mockFileUrl,
+        key: '1/test-file.txt',
+        size: 2048
+      });
+    });
+
+    it('should throw error when file record not found', async () => {
+      const uploadData = {
+        uploadId: 'upload-123',
+        bucket: 'd8dai',
+        key: '1/nonexistent.txt',
+        parts: [{ partNumber: 1, etag: 'etag1' }]
+      };
+
+      const mockCompleteMultipartUpload = vi.fn().mockResolvedValue({ size: 1024 });
+
+      vi.mocked(MinioService).mockImplementation(() => ({
+        completeMultipartUpload: mockCompleteMultipartUpload
+      } as unknown as MinioService));
+
+      const mockRepository = {
+        findOneBy: vi.fn().mockResolvedValue(null)
+      };
+
+      mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository);
+      const fileService = new FileService(mockDataSource);
+
+      await expect(fileService.completeMultipartUpload(uploadData)).rejects.toThrow('文件记录不存在');
+    });
+
+    it('should handle errors during completion', async () => {
+      const uploadData = {
+        uploadId: 'upload-123',
+        bucket: 'd8dai',
+        key: '1/test-file.txt',
+        parts: [{ partNumber: 1, etag: 'etag1' }]
+      };
+
+      const mockFile = {
+        id: 1,
+        path: '1/test-file.txt',
+        size: 0,
+        updatedAt: new Date()
+      } as File;
+
+      const mockRepository = {
+        findOneBy: vi.fn().mockResolvedValue(mockFile),
+        save: vi.fn()
+      };
+
+      const mockCompleteMultipartUpload = vi.fn().mockRejectedValue(new Error('Completion failed'));
+
+      mockDataSource.getRepository = vi.fn().mockReturnValue(mockRepository);
+      vi.mocked(MinioService).mockImplementation(() => ({
+        completeMultipartUpload: mockCompleteMultipartUpload
+      } as unknown as MinioService));
+
+      const fileService = new FileService(mockDataSource);
+
+      await expect(fileService.completeMultipartUpload(uploadData)).rejects.toThrow('完成分片上传失败');
+      expect(logger.error).toHaveBeenCalled();
+    });
+  });
+});