diff --git a/packages/common/src/services/file-upload.service.ts b/packages/common/src/services/file-upload.service.ts index 8ccc13d..dd34805 100644 --- a/packages/common/src/services/file-upload.service.ts +++ b/packages/common/src/services/file-upload.service.ts @@ -26,7 +26,7 @@ export class FileUploadService { private readonly storage: any; private readonly storageMode: string; private readonly useSSL: boolean; - private readonly storageEndpoint: string; + private readonly storageEndpoint: string; private readonly storagePort: number; private logger: Logger; @@ -219,52 +219,52 @@ export class FileUploadService { return destination; } - // async uploadMultiple( - // files: ReadonlyArray, - // destination: string, - // filenames: string[], - // ): Promise { - // const directories: string[] = []; + async uploadMultiple( + files: MultipartFile[], + destination: string, + filenames: string[], + ): Promise { + const directories: string[] = []; - // if (!files || files.length == 0) { - // this.logger.error(`Error uploading file: : 'files' field missing`); - // throw new InternalServerErrorException( - // 'File upload failed: files field missing', - // ); - // } - // if (!filenames) { - // this.logger.error(`Error uploading file: : 'filenames' field missing`); - // throw new InternalServerErrorException( - // 'File upload failed: filenames field missing', - // ); - // } - // if (!Array.isArray(filenames)) { - // filenames = [filenames]; - // } - // if (filenames.length != files.length) { - // this.logger.error( - // `Error uploading file: Number of files is not equal to number of filenames`, - // ); - // throw new InternalServerErrorException( - // 'File upload failed: Number of files is not equal to number of filenames', - // ); - // } - // let c: number = 0; - // for (const file of files) { - // try { - // const fileUploadRequestDto: FileUploadRequestDTO = - // new FileUploadRequestDTO(); - // fileUploadRequestDto.file = file; - // fileUploadRequestDto.destination = destination; - // fileUploadRequestDto.filename = filenames[c]; - // const directory = await this.upload(fileUploadRequestDto); - // directories.push(directory); - // } catch (error) { - // this.logger.error(`Error uploading file: ${error}`); - // throw new InternalServerErrorException('File upload failed'); - // } - // c++; - // } - // return directories; - // } + if (!files || files.length == 0) { + this.logger.error(`Error uploading file: : 'files' field missing`); + throw new InternalServerErrorException( + 'File upload failed: files field missing', + ); + } + if (!filenames) { + this.logger.error(`Error uploading file: : 'filenames' field missing`); + throw new InternalServerErrorException( + 'File upload failed: filenames field missing', + ); + } + if (!Array.isArray(filenames)) { + filenames = [filenames]; + } + if (filenames.length != files.length) { + this.logger.error( + `Error uploading file: Number of files is not equal to number of filenames`, + ); + throw new InternalServerErrorException( + 'File upload failed: Number of files is not equal to number of filenames', + ); + } + let counter: number = 0; + for (const file of files) { + try { + const fileUploadRequestDto: FileUploadRequestDTO = + new FileUploadRequestDTO(); + fileUploadRequestDto.file = file; + fileUploadRequestDto.destination = destination; + fileUploadRequestDto.filename = filenames[counter]; + const directory = await this.upload(fileUploadRequestDto); + directories.push(directory); + } catch (error) { + this.logger.error(`Error uploading file: ${error}`); + throw new InternalServerErrorException('File upload failed'); + } + counter++; + } + return directories; + } } diff --git a/packages/common/test/file-upload/file-upload.service.spec.ts b/packages/common/test/file-upload/file-upload.service.spec.ts index a017999..ea62a52 100644 --- a/packages/common/test/file-upload/file-upload.service.spec.ts +++ b/packages/common/test/file-upload/file-upload.service.spec.ts @@ -1,13 +1,20 @@ import { FileUploadService } from '../../src/services/file-upload.service'; -import { BadRequestException, InternalServerErrorException, Logger } from '@nestjs/common'; +import { + BadRequestException, + InternalServerErrorException, + Logger, + NotFoundException, +} from '@nestjs/common'; import { Client } from 'minio'; import * as fs from 'fs'; import * as path from 'path'; import { FileUploadRequestDTO, SaveToLocaleRequestDTO, + UploadToMinioRequestDTO, } from 'src/services/dto/file-upload.dto'; import { ConfigService } from '@nestjs/config'; +import { MultipartFile } from 'src'; jest.mock('minio'); jest.mock('fs'); @@ -18,6 +25,8 @@ describe('FileUploadService', () => { const mockMinioClient = { putObject: jest.fn(), getObject: jest.fn(), + bucketExists: jest.fn(), + makeBucket: jest.fn(), }; const mockLogger = { log: jest.fn(), @@ -25,132 +34,328 @@ describe('FileUploadService', () => { verbose: jest.fn(), }; const mockConfigService = { - get: jest.fn((key: string) => { - const config = { - STORAGE_MODE: 'MINIO', - // STORAGE_ENDPOINT: process.env.STORAGE_ENDPOINT || 'localhost', - // STORAGE_PORT:'9000', - // STORAGE_ACCESS_KEY: process.env.STORAGE_ACCESS_KEY, - // STORAGE_SECRET_KEY: process.env.STORAGE_SECRET_KEY, - // MINIO_BUCKETNAME: process.env.MINIO_BUCKETNAME, - }; - return config[key]; + get: jest.fn().mockImplementation((key: string) => { + switch (key) { + case 'STORAGE_MODE': + return 'MINIO'; + case 'STORAGE_USE_SSL': + return 'true'; + case 'STORAGE_ENDPOINT': + return 'localhost'; + case 'STORAGE_PORT': + return '9000'; + case 'STORAGE_ACCESS_KEY': + return 'accessKey'; + case 'STORAGE_SECRET_KEY': + return 'secretKey'; + default: + return null; + } }), }; beforeEach(() => { - (Client as jest.Mock).mockImplementation(() => mockMinioClient); - jest.spyOn(Logger.prototype, 'log').mockImplementation(mockLogger.log); - jest.spyOn(Logger.prototype, 'error').mockImplementation(mockLogger.error); - jest.spyOn(Logger.prototype, 'verbose').mockImplementation(mockLogger.verbose); - service = new FileUploadService(mockConfigService as unknown as ConfigService); - - (path.join as jest.Mock).mockImplementation((...paths) => paths.join('/')); - }); - - afterEach(() => { + (service as any).storage = mockMinioClient; jest.clearAllMocks(); - jest.resetModules(); }); - it('should be defined', () => { - expect(service).toBeDefined(); + describe('uploadToMinio', () => { + it('should upload file to Minio and return URL', async () => { + const uploadDto: UploadToMinioRequestDTO = { + destination: 'test-bucket', + filename: 'test-file.txt', + file: { buffer: Buffer.from('test content'), mimetype: 'text/plain' }, + }; + + mockMinioClient.bucketExists.mockResolvedValue(true); + mockMinioClient.putObject.mockImplementation((dest, file, buf, meta, cb) => { + cb(null); + }); + + const result = await service.uploadToMinio(uploadDto); + expect(result).toContain('https://localhost:9000/test-bucket/test-file.txt'); + expect(mockMinioClient.putObject).toHaveBeenCalledWith( + uploadDto.destination, + uploadDto.filename, + uploadDto.file.buffer, + { 'Content-Type': uploadDto.file.mimetype }, + expect.any(Function), + ); + }); + + it('should throw BadRequestException if bucket does not exist', async () => { + const uploadDto: UploadToMinioRequestDTO = { + destination: 'test-bucket', + filename: 'test-file.txt', + file: { buffer: Buffer.from('test content'), mimetype: 'text/plain' }, + }; + + mockMinioClient.bucketExists.mockResolvedValue(false); + + await expect(service.uploadToMinio(uploadDto)).rejects.toThrow(BadRequestException); + }); + + it('should log error and reject on upload error', async () => { + const uploadDto: UploadToMinioRequestDTO = { + destination: 'test-bucket', + filename: 'test-file.txt', + file: { buffer: Buffer.from('test content'), mimetype: 'text/plain' }, + }; + + mockMinioClient.bucketExists.mockResolvedValue(true); + mockMinioClient.putObject.mockImplementation((dest, file, buf, meta, cb) => { + cb(new Error('Upload error')); + }); + + await expect(service.uploadToMinio(uploadDto)).rejects.toThrow(Error); + }); }); describe('saveLocalFile', () => { - const mockFile = { - buffer: Buffer.from('test file'), - }; - const mockDestination = 'uploads'; - const mockFilename = 'test.txt'; - - beforeEach(() => { - (fs.existsSync as jest.Mock).mockReturnValue(true); - (fs.writeFileSync as jest.Mock).mockImplementation(() => { }); + it('should save file locally and return file path', async () => { + const saveDto: SaveToLocaleRequestDTO = { + destination: 'uploads', + filename: 'local-file.txt', + file: { buffer: Buffer.from('local content') }, + }; + + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + + const result = await service.saveLocalFile(saveDto); + expect(result).toBe('uploads/local-file.txt'); }); - it('should save a file locally', async () => { - const saveToLocaleRequestDto: SaveToLocaleRequestDTO = { - destination: mockDestination, - filename: mockFilename, - file: mockFile, + it('should throw BadRequestException for invalid destination path', async () => { + const saveDto: SaveToLocaleRequestDTO = { + destination: 'invalid.path', + filename: 'local-file.txt', + file: { buffer: Buffer.from('local content') }, }; - const result = await service.saveLocalFile(saveToLocaleRequestDto); - expect(result).toEqual(`${mockDestination}/${mockFilename}`); - expect(fs.existsSync).toHaveBeenCalledWith( - expect.stringContaining(mockDestination), - ); - expect(fs.writeFileSync).toHaveBeenCalledWith( - expect.stringContaining(`${mockDestination}/${mockFilename}`), - mockFile.buffer, - ); + + await expect(service.saveLocalFile(saveDto)).rejects.toThrow(BadRequestException); }); - it('should handle directory errors', async () => { - (fs.existsSync as jest.Mock).mockReturnValue(false); // Simulate destination path exists - (fs.mkdirSync as jest.Mock).mockImplementation(() => { - throw new Error('Directory creation error'); - }); - const saveToLocaleRequestDto: SaveToLocaleRequestDTO = { - destination: mockDestination, - filename: mockFilename, - file: mockFile, + it('should throw BadRequestException for invalid filename', async () => { + const saveDto: SaveToLocaleRequestDTO = { + destination: 'uploads', + filename: 'invalid*file.txt', + file: { buffer: Buffer.from('local content') }, }; - await expect(service.saveLocalFile(saveToLocaleRequestDto)).rejects.toThrow( - new BadRequestException('Given destination path does not exist. Please create one.'), - ); + + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + + await expect(service.saveLocalFile(saveDto)).rejects.toThrow(BadRequestException); + }); + + it('should throw BadRequestException if destination directory does not exist', async () => { + const saveDto: SaveToLocaleRequestDTO = { + destination: 'uploads', + filename: 'local-file.txt', + file: { buffer: Buffer.from('local content') }, + }; + + jest.spyOn(fs, 'existsSync').mockReturnValue(false); + + await expect(service.saveLocalFile(saveDto)).rejects.toThrow(BadRequestException); }); }); describe('upload', () => { - it('should upload a file to Minio if STORAGE_MODE is minio', async () => { - const file = { - buffer: Buffer.from('test file'), - mimetype: 'text/plain', - }; - const filename = 'test.txt'; - const destination = 'uploads'; - const expectedUrl = `http://${mockConfigService.get('STORAGE_ENDPOINT')}:${mockConfigService.get('STORAGE_PORT')}/${mockConfigService.get('MINIO_BUCKETNAME')}/${filename}`; - - jest - .spyOn(service as any, 'uploadToMinio') - .mockResolvedValue(expectedUrl); - const fileUploadDTO: FileUploadRequestDTO = { - destination, - filename, - file, + it('should call uploadToMinio for Minio storage mode', async () => { + const uploadDto: FileUploadRequestDTO = { + destination: 'test-bucket', + filename: 'test-file.txt', + file: { buffer: Buffer.from('test content'), mimetype: 'text/plain' }, }; - const result = await service.upload(fileUploadDTO); - expect(result).toEqual(expectedUrl); - expect(service.uploadToMinio).toHaveBeenCalledWith(fileUploadDTO); + mockMinioClient.bucketExists.mockResolvedValue(true); + mockMinioClient.putObject.mockImplementation((dest, file, buf, meta, cb) => { + cb(null); + }); + + const result = await service.upload(uploadDto); + expect(result).toContain('https://localhost:9000/test-bucket/test-file.txt'); }); - it('should handle upload errors', async () => { - const file = { - buffer: Buffer.from('test file'), - mimetype: 'text/plain', + it('should call saveLocalFile for local storage mode', async () => { + (service as any).storageMode = 'local'; + const saveDto: FileUploadRequestDTO = { + destination: 'uploads', + filename: 'local-file.txt', + file: { buffer: Buffer.from('local content'), mimetype: 'text/plain' }, }; - const filename = 'test.txt'; + + jest.spyOn(service, 'saveLocalFile').mockResolvedValue('uploads/local-file.txt'); + const result = await service.upload(saveDto); + expect(result).toBe('uploads/local-file.txt'); + }); + + it('should throw InternalServerErrorException for invalid storage mode', async () => { + (service as any).storageMode = 'INVALID_MODE'; + await expect(service.upload({} as FileUploadRequestDTO)).rejects.toThrow(InternalServerErrorException); + }); + }); + + describe('download', () => { + it('should return a stream from local storage', async () => { + (service as any).storageMode = 'local'; + const filename = 'local-file.txt'; const destination = 'uploads'; + const mockReadStream = 'mock stream' as any; - jest - .spyOn(service as any, 'uploadToMinio') - .mockRejectedValue(new Error('Upload error')); + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'createReadStream').mockReturnValue(mockReadStream); - const fileUploadDTO: FileUploadRequestDTO = { - destination: destination, - filename: filename, - file: file, - }; + const result = await service.download(destination, filename); - await expect(service.upload(fileUploadDTO)).rejects.toThrow( - InternalServerErrorException, - ); - expect(mockLogger.error).toHaveBeenCalledWith( - 'Error uploading file: Upload error', - ); + expect(result).toBe(mockReadStream); + }); + + it('should download from Minio storage', async () => { + const filename = 'test-file.txt'; + const destination = 'test-bucket'; + const mockObject = 'mock object' as any; + + mockMinioClient.getObject.mockResolvedValue(mockObject); + + const result = await service.download(destination, filename); + expect(result).toBe(mockObject); + }); + + it('should throw InternalServerErrorException on Minio download error', async () => { + const filename = 'test-file.txt'; + const destination = 'test-bucket'; + + mockMinioClient.getObject.mockImplementation(() => { + throw new Error('Download error'); + }); + + await expect(service.download(destination, filename)).rejects.toThrow(InternalServerErrorException); }); }); + + describe('makeBucket', () => { + it('should create a new bucket if it does not exist', async () => { + const bucketName = 'new-bucket'; + + mockMinioClient.bucketExists.mockResolvedValue(false); + mockMinioClient.makeBucket.mockResolvedValue(undefined); + + await service.makeBucket(bucketName); + expect(mockMinioClient.makeBucket).toHaveBeenCalledWith(bucketName); + }); + + it('should throw InternalServerErrorException if bucket creation fails', async () => { + const bucketName = 'new-bucket'; + + mockMinioClient.bucketExists.mockResolvedValue(false); + mockMinioClient.makeBucket.mockImplementation(() => { + throw new Error('Creation error'); + }); + + await expect(service.makeBucket(bucketName)).rejects.toThrow(InternalServerErrorException); + }); + + it('should throw an error if the bucket already exists', async () => { + const bucketName = 'existing-bucket'; + + mockMinioClient.bucketExists.mockResolvedValue(true); + + await expect(service.makeBucket(bucketName)).rejects.toThrow(InternalServerErrorException); + }); + }); + it('should create a directory for local storage', async () => { + (service as any).storageMode = 'local'; + (service as any).storageEndpoint = 'uploads'; + + const destination = 'local-destination'; + const mockUploadsDir = path.join(service['storageEndpoint'], destination); + + const mkdirSpy = jest.spyOn(fs, 'mkdirSync').mockImplementation(() => ''); + + await service.makeBucket(destination); + + expect(mkdirSpy).toHaveBeenCalledWith(mockUploadsDir, { recursive: true }); +}); + +describe('upload Mutiple', () => { + it('should throw an error if files are missing', async () => { + await expect( + service.uploadMultiple(null, 'uploads', ['file1.txt']), + ).rejects.toThrow( + new InternalServerErrorException('File upload failed: files field missing'), + ); + }); + + it('should throw an error if filenames are missing', async () => { + const mockFiles: MultipartFile[] = [{ filename: 'file1.txt' } as any]; + + await expect(service.uploadMultiple(mockFiles, 'uploads', null)).rejects.toThrow( + new InternalServerErrorException('File upload failed: filenames field missing'), + ); + }); + + it('should not throw an error if filenames is not an array but can be converted to one', async () => { + const mockFiles: MultipartFile[] = [{ filename: 'file1.txt' } as any]; + + const mockUploadFn = jest.spyOn(service, 'upload').mockResolvedValue('https://localhost:9000/uploads/file1.txt'); + + const result = await service.uploadMultiple(mockFiles, 'uploads', ['file1.txt']); + + expect(mockUploadFn).toHaveBeenCalledTimes(1); + expect(result).toEqual(['https://localhost:9000/uploads/file1.txt']); + }); + + it('should throw an error if filenames and files count do not match', async () => { + const mockFiles: MultipartFile[] = [ + { filename: 'file1.txt' } as any, + { filename: 'file2.txt' } as any, + ]; + + await expect( + service.uploadMultiple(mockFiles, 'uploads', ['file1.txt']), + ).rejects.toThrow( + new InternalServerErrorException( + 'File upload failed: Number of files is not equal to number of filenames', + ), + ); + }); + + it('should successfully upload multiple files', async () => { + const mockFiles: MultipartFile[] = [ + { filename: 'file1.txt' } as any, + { filename: 'file2.txt' } as any, + ]; + + const filenames = ['file1.txt', 'file2.txt']; + + const mockUploadFn = jest.spyOn(service, 'upload').mockResolvedValue('mock-directory'); + + const directories = await service.uploadMultiple(mockFiles, 'uploads', filenames); + + expect(mockUploadFn).toHaveBeenCalledTimes(mockFiles.length); + expect(directories.length).toBe(mockFiles.length); + expect(directories).toEqual(['mock-directory', 'mock-directory']); + }); + + it('should throw an error if any file upload fails', async () => { + const mockFiles: MultipartFile[] = [ + { filename: 'file1.txt' } as any, + { filename: 'file2.txt' } as any, + ]; + + const filenames = ['file1.txt', 'file2.txt']; + + jest.spyOn(service, 'upload') + .mockResolvedValueOnce('mock-directory') + .mockRejectedValueOnce(new Error('Upload failed')); + + await expect( + service.uploadMultiple(mockFiles, 'uploads', filenames), + ).rejects.toThrow(new InternalServerErrorException('File upload failed')); + }); + +}) + }); diff --git a/packages/common/test/interceptor/file-upload.interceptor.spec.ts b/packages/common/test/interceptor/file-upload.interceptor.spec.ts index b296e67..b94cd9a 100644 --- a/packages/common/test/interceptor/file-upload.interceptor.spec.ts +++ b/packages/common/test/interceptor/file-upload.interceptor.spec.ts @@ -1,129 +1,170 @@ import { ExecutionContext, NestInterceptor } from '@nestjs/common'; import { of, throwError } from 'rxjs'; -import { FastifyFilesInterceptor } from '../../src/interceptors/file-upload.interceptor'; +import { FastifyFilesInterceptor, FastifyFileInterceptor } from '../../src/interceptors/file-upload.interceptor'; -describe('FastifyFileInterceptor', () => { - let interceptor: NestInterceptor; +describe('FastifyFileInterceptor & FastifyFilesInterceptor', () => { + let fileInterceptor: NestInterceptor; + let filesInterceptor: NestInterceptor; beforeEach(async () => { - const interceptorClass = FastifyFilesInterceptor('file', []); - interceptor = new interceptorClass(); - }); + const fileInterceptorClass = FastifyFileInterceptor('file', {}); + const filesInterceptorClass = FastifyFilesInterceptor('files', []); - it('should be defined', () => { - expect(interceptor).toBeDefined(); + fileInterceptor = new fileInterceptorClass(); + filesInterceptor = new filesInterceptorClass(); }); - // file upload tests + describe('FastifyFileInterceptor', () => { + it('should be defined', () => { + expect(fileInterceptor).toBeDefined(); + }); - it('should handle file upload', async () => { - const file = { originalname: 'test.jpg', mimetype: 'image/jpeg' }; - const context = createMockContext(file); - const nextHandler = createMockNextHandler(); + it('should handle single file upload', async () => { + const file = { originalname: 'test.jpg', mimetype: 'image/jpeg' }; + const context = createMockContext(file); + const nextHandler = createMockNextHandler(); - await interceptor.intercept(context, nextHandler); + await fileInterceptor.intercept(context, nextHandler); - expect(context.switchToHttp().getRequest().file).toEqual(file); - expect(nextHandler.handle).toHaveBeenCalled(); - }); + expect(context.switchToHttp().getRequest().file).toEqual(file); + expect(nextHandler.handle).toHaveBeenCalled(); + }); + + it('should handle file upload without file extension', async () => { + const file = { originalname: 'test', mimetype: 'application/octet-stream' }; + const context = createMockContext(file); + const nextHandler = createMockNextHandler(); + + await fileInterceptor.intercept(context, nextHandler); + + expect(context.switchToHttp().getRequest().file).toEqual(file); + expect(nextHandler.handle).toHaveBeenCalled(); + }); + + it('should throw an error for unsupported file types', async () => { + const file = { originalname: 'test.exe', mimetype: 'application/x-msdownload' }; + const context = createMockContext(file); + const nextHandler = createMockNextHandler(); + + jest.spyOn(fileInterceptor['multer'], 'single').mockImplementation(() => { + return (req, res, callback) => { + callback(new Error('Unsupported file type')); + }; + }); + + await expect(fileInterceptor.intercept(context, nextHandler)).rejects.toThrow('Unsupported file type'); + }); - it('should accept files with simple names', async () => { - const file = { originalname: 'test.txt', mimetype: 'text/plain' }; - const context = createMockContext(file); - const nextHandler = createMockNextHandler(); + it('should handle empty file', async () => { + const file = { originalname: 'empty.txt', mimetype: 'text/plain', size: 0 }; + const context = createMockContext(file); + const nextHandler = createMockNextHandler(); - await interceptor.intercept(context, nextHandler); + await fileInterceptor.intercept(context, nextHandler); - expect(context.switchToHttp().getRequest().file).toEqual(file); - expect(nextHandler.handle).toHaveBeenCalled(); - }) + expect(context.switchToHttp().getRequest().file).toEqual(file); + expect(nextHandler.handle).toHaveBeenCalled(); + }); - it('should accept files with multiple periods in them', async () => { - const file = { originalname: 'text.tar.gz', mimetype: 'application/gzip' }; - const context = createMockContext(file); - const nextHandler = createMockNextHandler(); + it('should handle large file upload', async () => { + const file = { originalname: 'large.mp4', mimetype: 'video/mp4', size: 1000000000 }; + const context = createMockContext(file); + const nextHandler = createMockNextHandler(); - await interceptor.intercept(context, nextHandler); + await fileInterceptor.intercept(context, nextHandler); - expect(context.switchToHttp().getRequest().file).toEqual(file); - expect(nextHandler.handle).toHaveBeenCalled(); - }) + expect(context.switchToHttp().getRequest().file).toEqual(file); + expect(nextHandler.handle).toHaveBeenCalled(); + }); + }); - it('should accept files without extensions', async () => { - const file = { originalname: 'text', mimetype: 'text/plain' }; - const context = createMockContext(file); - const nextHandler = createMockNextHandler(); + describe('FastifyFilesInterceptor', () => { + it('should be defined', () => { + expect(filesInterceptor).toBeDefined(); + }); - await interceptor.intercept(context, nextHandler); + it('should handle multiple file uploads', async () => { + const files = [ + { originalname: 'test1.jpg', mimetype: 'image/jpeg' }, + { originalname: 'test2.jpg', mimetype: 'image/jpeg' } + ]; + const context = createMockContext(files); + const nextHandler = createMockNextHandler(); - expect(context.switchToHttp().getRequest().file).toEqual(file); - expect(nextHandler.handle).toHaveBeenCalled(); - }) + await filesInterceptor.intercept(context, nextHandler); - it('should accept files with spaces in their name', async () => { - const file = { originalname: 'data file.txt', mimetype: 'text/plain' }; - const context = createMockContext(file); - const nextHandler = createMockNextHandler(); + expect(context.switchToHttp().getRequest().files).toEqual(files); + expect(nextHandler.handle).toHaveBeenCalled(); + }); - await interceptor.intercept(context, nextHandler); + it('should handle an empty list of files', async () => { + const context = createMockContext([]); + const nextHandler = createMockNextHandler(); - expect(context.switchToHttp().getRequest().file).toEqual(file); - expect(nextHandler.handle).toHaveBeenCalled(); - }) + await filesInterceptor.intercept(context, nextHandler); - it('should throw Error uploading file on illegal filename', async () => { - const file = { originalname: '../foo.bar.cls', mimetype: 'text/plain' }; - const context = createMockContext(file); - const nextHandler = createMockNextHandler(); + expect(context.switchToHttp().getRequest().files).toEqual([]); + expect(nextHandler.handle).toHaveBeenCalled(); + }); - jest.spyOn(interceptor, 'intercept').mockImplementation(() => { - throw new Error('Illegal filename'); + it('should throw an error when too many files are uploaded', async () => { + const files = [ + { originalname: 'test1.jpg', mimetype: 'image/jpeg' }, + { originalname: 'test2.jpg', mimetype: 'image/jpeg' }, + { originalname: 'test3.jpg', mimetype: 'image/jpeg' } + ]; + const context = createMockContext(files); + const nextHandler = createMockNextHandler(); + + jest.spyOn(filesInterceptor['multer'], 'array').mockImplementation(() => { + return (req, res, callback) => { + callback(new Error('Too many files')); + }; + }); + + await expect(filesInterceptor.intercept(context, nextHandler)).rejects.toThrow('Too many files'); }); - try { - await interceptor.intercept(context, nextHandler); - } catch (error) { - expect(error).toEqual(new Error('Illegal filename')); - } - - expect(nextHandler.handle).not.toHaveBeenCalled(); - - }) - - it('should handle getting an uploaded file when file is not present or null', async () => { - const contextWithUndefinedFile = createMockContext(undefined); - const contextWithNullFile = createMockContext(null); - const nextHandler = createMockNextHandler(); - - await interceptor.intercept(contextWithUndefinedFile, nextHandler); - expect(contextWithUndefinedFile.switchToHttp().getRequest().file).toBeUndefined(); - expect(nextHandler.handle).toHaveBeenCalled(); - - await interceptor.intercept(contextWithNullFile, nextHandler); - expect(contextWithNullFile.switchToHttp().getRequest().file).toBeNull(); - expect(nextHandler.handle).toHaveBeenCalled(); - }); + it('should handle mix of file types', async () => { + const files = [ + { originalname: 'test1.jpg', mimetype: 'image/jpeg' }, + { originalname: 'test2.pdf', mimetype: 'application/pdf' } + ]; + const context = createMockContext(files); + const nextHandler = createMockNextHandler(); + + await filesInterceptor.intercept(context, nextHandler); + + expect(context.switchToHttp().getRequest().files).toEqual(files); + expect(nextHandler.handle).toHaveBeenCalled(); + }); - it('should handle errors', async () => { - const errorMessage = 'File upload failed'; - const file = { originalname: 'test.jpg', mimetype: 'image/jpeg' }; - const context = createMockContext(file); - const nextHandler = createMockNextHandler(); - - // Mock the multer middleware to throw an error - jest.spyOn(interceptor['multer'], 'array').mockImplementation(() => { - return (req, res, callback) => { - callback(new Error(errorMessage)); - }; + it('should handle error when one of the files fails to upload', async () => { + const files = [ + { originalname: 'test1.jpg', mimetype: 'image/jpeg' }, + { originalname: 'test2.pdf', mimetype: 'application/pdf' } + ]; + const context = createMockContext(files); + const nextHandler = createMockNextHandler(); + + jest.spyOn(filesInterceptor['multer'], 'array').mockImplementation(() => { + return (req, res, callback) => { + callback(new Error('File upload failed for test2.pdf')); + }; + }); + + await expect(filesInterceptor.intercept(context, nextHandler)).rejects.toThrow('File upload failed for test2.pdf'); }); - - await expect(interceptor.intercept(context, nextHandler)).rejects.toThrow(errorMessage); }); }); -function createMockContext(file: any): ExecutionContext { +function createMockContext(fileOrFiles: any): ExecutionContext { const mockHttpContext = { - getRequest: jest.fn().mockReturnValue({ raw: { headers: { 'content-type': 'multipart/form-data' } }, file }), + getRequest: jest.fn().mockReturnValue({ + raw: { headers: { 'content-type': 'multipart/form-data' } }, + file: Array.isArray(fileOrFiles) ? undefined : fileOrFiles, + files: Array.isArray(fileOrFiles) ? fileOrFiles : undefined + }), getResponse: jest.fn().mockReturnValue({}), }; return { switchToHttp: jest.fn().mockReturnValue(mockHttpContext) } as unknown as ExecutionContext;