diff --git a/package-lock.json b/package-lock.json index 2fcac93105a..0d4b7c9d241 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1619,8 +1619,7 @@ "@types/mocha": { "version": "5.2.6", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.6.tgz", - "integrity": "sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw==", - "dev": true + "integrity": "sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw==" }, "@types/node": { "version": "10.14.5", @@ -2124,6 +2123,11 @@ "buffer-equal": "^1.0.0" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, "append-transform": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", @@ -6108,6 +6112,69 @@ "qs": "^6.5.1" } }, + "fastify-multer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/fastify-multer/-/fastify-multer-1.4.2.tgz", + "integrity": "sha512-e9KY/ZkSTFGIyB6u/A7DTr5zee5lYAgsvyIdMf56JtXlMQnbDhQaGDUWzyrx31+I6MhZOC6Y1angjvj1mujmcw==", + "requires": { + "@types/mocha": "^5.2.6", + "append-field": "^1.0.0", + "busboy": "^0.3.0", + "concat-stream": "^2.0.0", + "fastify-plugin": "^1.5.0", + "mkdirp": "^0.5.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.16", + "xtend": "^4.0.1" + }, + "dependencies": { + "busboy": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz", + "integrity": "sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==", + "requires": { + "dicer": "0.3.0" + } + }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "dicer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", + "requires": { + "streamsearch": "0.1.2" + } + }, + "readable-stream": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz", + "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "fastify-multipart": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/fastify-multipart/-/fastify-multipart-0.8.1.tgz", diff --git a/package.json b/package.json index 4cd72823b97..035dd4b1204 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "fastify": "2.3.0", "fastify-cors": "2.1.2", "fastify-formbody": "3.1.0", + "fastify-multer": "^1.4.2", "fastify-multipart": "0.8.1", "graphql": "14.2.1", "grpc": "1.20.0", diff --git a/packages/platform-fastify/index.ts b/packages/platform-fastify/index.ts index 277f7c0eaf8..2b00533258f 100644 --- a/packages/platform-fastify/index.ts +++ b/packages/platform-fastify/index.ts @@ -7,3 +7,4 @@ export * from './adapters'; export * from './interfaces'; +export * from './multer'; diff --git a/packages/platform-fastify/multer/files.constants.ts b/packages/platform-fastify/multer/files.constants.ts new file mode 100644 index 00000000000..fd2ef8091cc --- /dev/null +++ b/packages/platform-fastify/multer/files.constants.ts @@ -0,0 +1 @@ +export const MULTER_MODULE_OPTIONS = 'MULTER_MODULE_OPTIONS'; diff --git a/packages/platform-fastify/multer/index.ts b/packages/platform-fastify/multer/index.ts new file mode 100644 index 00000000000..7afd4aab899 --- /dev/null +++ b/packages/platform-fastify/multer/index.ts @@ -0,0 +1,3 @@ +export * from './interceptors'; +export * from './interfaces'; +export * from './multer.module'; diff --git a/packages/platform-fastify/multer/interceptors/file-fields.interceptor.ts b/packages/platform-fastify/multer/interceptors/file-fields.interceptor.ts new file mode 100644 index 00000000000..0a523f74187 --- /dev/null +++ b/packages/platform-fastify/multer/interceptors/file-fields.interceptor.ts @@ -0,0 +1,64 @@ +import * as multer from 'fastify-multer'; +import { Observable } from 'rxjs'; +import { MULTER_MODULE_OPTIONS } from '../files.constants'; +import { MulterModuleOptions } from '../interfaces'; +import { transformException } from '../multer/multer.utils'; +import { + CallHandler, + ExecutionContext, + Inject, + mixin, + NestInterceptor, + Optional, + Type, +} from '@nestjs/common'; +import { + MulterField, + MulterOptions, +} from '../interfaces/multer-options.interface'; + +type MulterInstance = any; + +export function FileFieldsInterceptor( + uploadFields: MulterField[], + localOptions?: MulterOptions, +): Type { + class MixinInterceptor implements NestInterceptor { + protected multer: MulterInstance; + + constructor( + @Optional() + @Inject(MULTER_MODULE_OPTIONS) + options: MulterModuleOptions = {}, + ) { + this.multer = (multer as any)({ + ...options, + ...localOptions, + }); + } + + async intercept( + context: ExecutionContext, + next: CallHandler, + ): Promise> { + const ctx = context.switchToHttp(); + + await new Promise((resolve, reject) => + this.multer.fields(uploadFields)( + ctx.getRequest(), + ctx.getResponse(), + (err: any) => { + if (err) { + const error = transformException(err); + return reject(error); + } + resolve(); + }, + ), + ); + return next.handle(); + } + } + const Interceptor = mixin(MixinInterceptor); + return Interceptor as Type; +} diff --git a/packages/platform-fastify/multer/interceptors/file.interceptor.ts b/packages/platform-fastify/multer/interceptors/file.interceptor.ts new file mode 100644 index 00000000000..651ae9d28d7 --- /dev/null +++ b/packages/platform-fastify/multer/interceptors/file.interceptor.ts @@ -0,0 +1,61 @@ +import * as multer from 'fastify-multer'; +import { Observable } from 'rxjs'; +import { MULTER_MODULE_OPTIONS } from '../files.constants'; +import { MulterModuleOptions } from '../interfaces'; +import { MulterOptions } from '../interfaces/multer-options.interface'; +import { transformException } from '../multer/multer.utils'; +import { + CallHandler, + ExecutionContext, + Inject, + mixin, + NestInterceptor, + Optional, + Type, +} from '@nestjs/common'; + +type MulterInstance = any; + +export function FileInterceptor( + fieldName: string, + localOptions?: MulterOptions, +): Type { + class MixinInterceptor implements NestInterceptor { + protected multer: MulterInstance; + + constructor( + @Optional() + @Inject(MULTER_MODULE_OPTIONS) + options: MulterModuleOptions = {}, + ) { + this.multer = (multer as any)({ + ...options, + ...localOptions, + }); + } + + async intercept( + context: ExecutionContext, + next: CallHandler, + ): Promise> { + const ctx = context.switchToHttp(); + + await new Promise((resolve, reject) => + this.multer.single(fieldName)( + ctx.getRequest(), + ctx.getResponse(), + (err: any) => { + if (err) { + const error = transformException(err); + return reject(error); + } + resolve(); + }, + ), + ); + return next.handle(); + } + } + const Interceptor = mixin(MixinInterceptor); + return Interceptor as Type; +} diff --git a/packages/platform-fastify/multer/interceptors/files.interceptor.ts b/packages/platform-fastify/multer/interceptors/files.interceptor.ts new file mode 100644 index 00000000000..6b99794b1a0 --- /dev/null +++ b/packages/platform-fastify/multer/interceptors/files.interceptor.ts @@ -0,0 +1,62 @@ +import * as multer from 'fastify-multer'; +import { Observable } from 'rxjs'; +import { MULTER_MODULE_OPTIONS } from '../files.constants'; +import { MulterModuleOptions } from '../interfaces'; +import { MulterOptions } from '../interfaces/multer-options.interface'; +import { transformException } from '../multer/multer.utils'; +import { + CallHandler, + ExecutionContext, + Inject, + mixin, + NestInterceptor, + Optional, + Type, +} from '@nestjs/common'; + +type MulterInstance = any; + +export function FilesInterceptor( + fieldName: string, + maxCount?: number, + localOptions?: MulterOptions, +): Type { + class MixinInterceptor implements NestInterceptor { + protected multer: MulterInstance; + + constructor( + @Optional() + @Inject(MULTER_MODULE_OPTIONS) + options: MulterModuleOptions = {}, + ) { + this.multer = (multer as any)({ + ...options, + ...localOptions, + }); + } + + async intercept( + context: ExecutionContext, + next: CallHandler, + ): Promise> { + const ctx = context.switchToHttp(); + + await new Promise((resolve, reject) => + this.multer.array(fieldName, maxCount)( + ctx.getRequest(), + ctx.getResponse(), + (err: any) => { + if (err) { + const error = transformException(err); + return reject(error); + } + resolve(); + }, + ), + ); + return next.handle(); + } + } + const Interceptor = mixin(MixinInterceptor); + return Interceptor as Type; +} diff --git a/packages/platform-fastify/multer/interceptors/index.ts b/packages/platform-fastify/multer/interceptors/index.ts new file mode 100644 index 00000000000..3de0e24ce6d --- /dev/null +++ b/packages/platform-fastify/multer/interceptors/index.ts @@ -0,0 +1,3 @@ +export * from './file-fields.interceptor'; +export * from './file.interceptor'; +export * from './files.interceptor'; diff --git a/packages/platform-fastify/multer/interfaces/files-upload-module.interface.ts b/packages/platform-fastify/multer/interfaces/files-upload-module.interface.ts new file mode 100644 index 00000000000..832e8ed22a5 --- /dev/null +++ b/packages/platform-fastify/multer/interfaces/files-upload-module.interface.ts @@ -0,0 +1,19 @@ +import { Type } from '@nestjs/common'; +import { ModuleMetadata } from '@nestjs/common/interfaces'; +import { MulterOptions } from '../interfaces/multer-options.interface'; + +export interface MulterModuleOptions extends MulterOptions {} + +export interface MulterOptionsFactory { + createMulterOptions(): Promise | MulterModuleOptions; +} + +export interface MulterModuleAsyncOptions + extends Pick { + useExisting?: Type; + useClass?: Type; + useFactory?: ( + ...args: any[] + ) => Promise | MulterModuleOptions; + inject?: any[]; +} diff --git a/packages/platform-fastify/multer/interfaces/index.ts b/packages/platform-fastify/multer/interfaces/index.ts new file mode 100644 index 00000000000..be94ec24afd --- /dev/null +++ b/packages/platform-fastify/multer/interfaces/index.ts @@ -0,0 +1 @@ +export * from './files-upload-module.interface'; diff --git a/packages/platform-fastify/multer/interfaces/multer-options.interface.ts b/packages/platform-fastify/multer/interfaces/multer-options.interface.ts new file mode 100644 index 00000000000..0b8e80e38a3 --- /dev/null +++ b/packages/platform-fastify/multer/interfaces/multer-options.interface.ts @@ -0,0 +1,61 @@ +/** + * @see https://github.com/expressjs/multer + */ +export interface MulterOptions { + dest?: string; + /** The storage engine to use for uploaded files. */ + storage?: any; + /** + * An object specifying the size limits of the following optional properties. This object is passed to busboy + * directly, and the details of properties can be found on https://github.com/mscdex/busboy#busboy-methods + */ + limits?: { + /** Max field name size (Default: 100 bytes) */ + fieldNameSize?: number; + /** Max field value size (Default: 1MB) */ + fieldSize?: number; + /** Max number of non- file fields (Default: Infinity) */ + fields?: number; + /** For multipart forms, the max file size (in bytes)(Default: Infinity) */ + fileSize?: number; + /** For multipart forms, the max number of file fields (Default: Infinity) */ + files?: number; + /** For multipart forms, the max number of parts (fields + files)(Default: Infinity) */ + parts?: number; + /** For multipart forms, the max number of header key=> value pairs to parse Default: 2000(same as node's http). */ + headerPairs?: number; + /** Keep the full path of files instead of just the base name (Default: false) */ + preservePath?: boolean; + }; + fileFilter?( + req: any, + file: { + /** Field name specified in the form */ + fieldname: string; + /** Name of the file on the user's computer */ + originalname: string; + /** Encoding type of the file */ + encoding: string; + /** Mime type of the file */ + mimetype: string; + /** Size of the file in bytes */ + size: number; + /** The folder to which the file has been saved (DiskStorage) */ + destination: string; + /** The name of the file within the destination (DiskStorage) */ + filename: string; + /** Location of the uploaded file (DiskStorage) */ + path: string; + /** A Buffer of the entire file (MemoryStorage) */ + buffer: Buffer; + }, + callback: (error: Error | null, acceptFile: boolean) => void, + ): void; +} + +export interface MulterField { + /** The field name. */ + name: string; + /** Optional maximum number of files per field to accept. */ + maxCount?: number; +} diff --git a/packages/platform-fastify/multer/multer.module.ts b/packages/platform-fastify/multer/multer.module.ts new file mode 100644 index 00000000000..5d28a99e867 --- /dev/null +++ b/packages/platform-fastify/multer/multer.module.ts @@ -0,0 +1,60 @@ +import { DynamicModule, Module, Provider } from '@nestjs/common'; +import { MULTER_MODULE_OPTIONS } from './files.constants'; +import { + MulterModuleAsyncOptions, + MulterModuleOptions, + MulterOptionsFactory, +} from './interfaces/files-upload-module.interface'; + +@Module({}) +export class MulterModule { + static register(options: MulterModuleOptions = {}): DynamicModule { + return { + module: MulterModule, + providers: [{ provide: MULTER_MODULE_OPTIONS, useValue: options }], + exports: [MULTER_MODULE_OPTIONS], + }; + } + + static registerAsync(options: MulterModuleAsyncOptions): DynamicModule { + return { + module: MulterModule, + imports: options.imports, + providers: this.createAsyncProviders(options), + exports: [MULTER_MODULE_OPTIONS], + }; + } + + private static createAsyncProviders( + options: MulterModuleAsyncOptions, + ): Provider[] { + if (options.useExisting || options.useFactory) { + return [this.createAsyncOptionsProvider(options)]; + } + return [ + this.createAsyncOptionsProvider(options), + { + provide: options.useClass, + useClass: options.useClass, + }, + ]; + } + + private static createAsyncOptionsProvider( + options: MulterModuleAsyncOptions, + ): Provider { + if (options.useFactory) { + return { + provide: MULTER_MODULE_OPTIONS, + useFactory: options.useFactory, + inject: options.inject || [], + }; + } + return { + provide: MULTER_MODULE_OPTIONS, + useFactory: async (optionsFactory: MulterOptionsFactory) => + optionsFactory.createMulterOptions(), + inject: [options.useExisting || options.useClass], + }; + } +} diff --git a/packages/platform-fastify/multer/multer/multer.constants.ts b/packages/platform-fastify/multer/multer/multer.constants.ts new file mode 100644 index 00000000000..e3c7b4557f2 --- /dev/null +++ b/packages/platform-fastify/multer/multer/multer.constants.ts @@ -0,0 +1,9 @@ +export const multerExceptions = { + LIMIT_PART_COUNT: 'Too many parts', + LIMIT_FILE_SIZE: 'File too large', + LIMIT_FILE_COUNT: 'Too many files', + LIMIT_FIELD_KEY: 'Field name too long', + LIMIT_FIELD_VALUE: 'Field value too long', + LIMIT_FIELD_COUNT: 'Too many fields', + LIMIT_UNEXPECTED_FILE: 'Unexpected field', +}; diff --git a/packages/platform-fastify/multer/multer/multer.utils.ts b/packages/platform-fastify/multer/multer/multer.utils.ts new file mode 100644 index 00000000000..81e19739ca9 --- /dev/null +++ b/packages/platform-fastify/multer/multer/multer.utils.ts @@ -0,0 +1,24 @@ +import { + BadRequestException, + HttpException, + PayloadTooLargeException, +} from '@nestjs/common'; +import { multerExceptions } from './multer.constants'; + +export function transformException(error: Error | undefined) { + if (!error || error instanceof HttpException) { + return error; + } + switch (error.message) { + case multerExceptions.LIMIT_FILE_SIZE: + return new PayloadTooLargeException(error.message); + case multerExceptions.LIMIT_FILE_COUNT: + case multerExceptions.LIMIT_FIELD_KEY: + case multerExceptions.LIMIT_FIELD_VALUE: + case multerExceptions.LIMIT_FIELD_COUNT: + case multerExceptions.LIMIT_UNEXPECTED_FILE: + case multerExceptions.LIMIT_PART_COUNT: + return new BadRequestException(error.message); + } + return error; +} diff --git a/packages/platform-fastify/package.json b/packages/platform-fastify/package.json index c4851c49417..13fd6a18bd8 100644 --- a/packages/platform-fastify/package.json +++ b/packages/platform-fastify/package.json @@ -15,6 +15,8 @@ "fastify": "2.3.0", "fastify-cors": "2.1.2", "fastify-formbody": "3.1.0", + "fastify-multer": "^1.4.2", + "fastify-multipart": "^0.8.1", "path-to-regexp": "3.0.0" }, "peerDependencies": { diff --git a/packages/platform-fastify/test/multer/interceptors/file-fields.interceptor.spec.ts b/packages/platform-fastify/test/multer/interceptors/file-fields.interceptor.spec.ts new file mode 100644 index 00000000000..7ab6fea50df --- /dev/null +++ b/packages/platform-fastify/test/multer/interceptors/file-fields.interceptor.spec.ts @@ -0,0 +1,65 @@ +import { CallHandler } from '@nestjs/common'; +import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; +import { expect } from 'chai'; +import { of } from 'rxjs'; +import * as sinon from 'sinon'; +import { FileFieldsInterceptor } from '../../../multer/interceptors/file-fields.interceptor'; + +describe('FileFieldsInterceptor', () => { + it('should return metatype with expected structure', async () => { + const targetClass = FileFieldsInterceptor([ + { name: 'file', maxCount: 1 }, + { name: 'anotherFile', maxCount: 1 }, + ]); + expect(targetClass.prototype.intercept).to.not.be.undefined; + }); + describe('intercept', () => { + let handler: CallHandler; + beforeEach(() => { + handler = { + handle: () => of('test'), + }; + }); + it('should call object with expected params', async () => { + const fieldName1 = 'file'; + const maxCount1 = 1; + const fieldName2 = 'anotherFile'; + const maxCount2 = 2; + const argument = [ + { name: fieldName1, maxCount: maxCount1 }, + { name: fieldName2, maxCount: maxCount2 }, + ]; + const target = new (FileFieldsInterceptor(argument))(); + + const callback = (req, res, next) => next(); + const fieldsSpy = sinon + .stub((target as any).multer, 'fields') + .returns(callback); + + await target.intercept(new ExecutionContextHost([]), handler); + + expect(fieldsSpy.called).to.be.true; + expect(fieldsSpy.calledWith(argument)).to.be.true; + }); + it('should transform exception', async () => { + const fieldName1 = 'file'; + const maxCount1 = 1; + const fieldName2 = 'anotherFile'; + const maxCount2 = 2; + const argument = [ + { name: fieldName1, maxCount: maxCount1 }, + { name: fieldName2, maxCount: maxCount2 }, + ]; + const target = new (FileFieldsInterceptor(argument))(); + const err = {}; + const callback = (req, res, next) => next(err); + + (target as any).fields = { + array: () => callback, + }; + (target.intercept(new ExecutionContextHost([]), handler) as any).catch( + error => expect(error).to.not.be.undefined, + ); + }); + }); +}); diff --git a/packages/platform-fastify/test/multer/interceptors/file.interceptor.spec.ts b/packages/platform-fastify/test/multer/interceptors/file.interceptor.spec.ts new file mode 100644 index 00000000000..7f4178db44e --- /dev/null +++ b/packages/platform-fastify/test/multer/interceptors/file.interceptor.spec.ts @@ -0,0 +1,47 @@ +import { CallHandler } from '@nestjs/common'; +import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; +import { expect } from 'chai'; +import { of } from 'rxjs'; +import * as sinon from 'sinon'; +import { FileInterceptor } from '../../../multer/interceptors/file.interceptor'; + +describe('FileInterceptor', () => { + it('should return metatype with expected structure', async () => { + const targetClass = FileInterceptor('file'); + expect(targetClass.prototype.intercept).to.not.be.undefined; + }); + describe('intercept', () => { + let handler: CallHandler; + beforeEach(() => { + handler = { + handle: () => of('test'), + }; + }); + it('should call single() with expected params', async () => { + const fieldName = 'file'; + const target = new (FileInterceptor(fieldName))(); + const callback = (req, res, next) => next(); + const singleSpy = sinon + .stub((target as any).multer, 'single') + .returns(callback); + + await target.intercept(new ExecutionContextHost([]), handler); + + expect(singleSpy.called).to.be.true; + expect(singleSpy.calledWith(fieldName)).to.be.true; + }); + it('should transform exception', async () => { + const fieldName = 'file'; + const target = new (FileInterceptor(fieldName))(); + const err = {}; + const callback = (req, res, next) => next(err); + + (target as any).multer = { + single: () => callback, + }; + (target.intercept(new ExecutionContextHost([]), handler) as any).catch( + error => expect(error).to.not.be.undefined, + ); + }); + }); +}); diff --git a/packages/platform-fastify/test/multer/interceptors/files.interceptor.spec.ts b/packages/platform-fastify/test/multer/interceptors/files.interceptor.spec.ts new file mode 100644 index 00000000000..c6caadd62aa --- /dev/null +++ b/packages/platform-fastify/test/multer/interceptors/files.interceptor.spec.ts @@ -0,0 +1,49 @@ +import { CallHandler } from '@nestjs/common'; +import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; +import { expect } from 'chai'; +import { of } from 'rxjs'; +import * as sinon from 'sinon'; +import { FilesInterceptor } from '../../../multer/interceptors/files.interceptor'; + +describe('FilesInterceptor', () => { + it('should return metatype with expected structure', async () => { + const targetClass = FilesInterceptor('file'); + expect(targetClass.prototype.intercept).to.not.be.undefined; + }); + describe('intercept', () => { + let handler: CallHandler; + beforeEach(() => { + handler = { + handle: () => of('test'), + }; + }); + it('should call array() with expected params', async () => { + const fieldName = 'file'; + const maxCount = 10; + const target = new (FilesInterceptor(fieldName, maxCount))(); + + const callback = (req, res, next) => next(); + const arraySpy = sinon + .stub((target as any).multer, 'array') + .returns(callback); + + await target.intercept(new ExecutionContextHost([]), handler); + + expect(arraySpy.called).to.be.true; + expect(arraySpy.calledWith(fieldName, maxCount)).to.be.true; + }); + it('should transform exception', async () => { + const fieldName = 'file'; + const target = new (FilesInterceptor(fieldName))(); + const err = {}; + const callback = (req, res, next) => next(err); + + (target as any).multer = { + array: () => callback, + }; + (target.intercept(new ExecutionContextHost([]), handler) as any).catch( + error => expect(error).to.not.be.undefined, + ); + }); + }); +}); diff --git a/packages/platform-fastify/test/multer/multer/multer.module.spec.ts b/packages/platform-fastify/test/multer/multer/multer.module.spec.ts new file mode 100644 index 00000000000..759110a927e --- /dev/null +++ b/packages/platform-fastify/test/multer/multer/multer.module.spec.ts @@ -0,0 +1,83 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { MULTER_MODULE_OPTIONS } from '../../../multer/files.constants'; +import { MulterModule } from '../../../multer/multer.module'; + +describe('MulterModule', () => { + describe('register', () => { + it('should provide an options', () => { + const options = { + test: 'test', + }; + const dynamicModule = MulterModule.register(options as any); + + expect(dynamicModule.providers).to.have.length(1); + expect(dynamicModule.imports).to.be.undefined; + expect(dynamicModule.exports).to.include(MULTER_MODULE_OPTIONS); + expect(dynamicModule.providers).to.deep.include({ + provide: MULTER_MODULE_OPTIONS, + useValue: options, + }); + }); + }); + + describe('register async', () => { + describe('when useFactory', () => { + it('should provide an options', () => { + const options: any = {}; + const asyncOptions = { + useFactory: () => options, + }; + const dynamicModule = MulterModule.registerAsync(asyncOptions); + + expect(dynamicModule.providers).to.have.length(1); + expect(dynamicModule.imports).to.be.undefined; + expect(dynamicModule.exports).to.include(MULTER_MODULE_OPTIONS); + expect(dynamicModule.providers).to.deep.include({ + provide: MULTER_MODULE_OPTIONS, + useFactory: asyncOptions.useFactory, + inject: [], + }); + }); + }); + + describe('when useExisting', () => { + it('should provide an options', () => { + const asyncOptions = { + useExisting: Object, + }; + const dynamicModule = MulterModule.registerAsync(asyncOptions as any); + + expect(dynamicModule.providers).to.have.length(1); + expect(dynamicModule.imports).to.be.undefined; + expect(dynamicModule.exports).to.include(MULTER_MODULE_OPTIONS); + }); + }); + + describe('when useClass', () => { + it('should provide an options', () => { + const asyncOptions = { + useClass: Object, + }; + const dynamicModule = MulterModule.registerAsync(asyncOptions as any); + + expect(dynamicModule.providers).to.have.length(2); + expect(dynamicModule.imports).to.be.undefined; + expect(dynamicModule.exports).to.include(MULTER_MODULE_OPTIONS); + }); + it('provider should call "createMulterOptions"', async () => { + const asyncOptions = { + useClass: Object, + }; + const dynamicModule = MulterModule.registerAsync(asyncOptions as any); + const optionsFactory = { + createMulterOptions: sinon.spy(), + }; + await ((dynamicModule.providers[0] as any).useFactory as any)( + optionsFactory, + ); + expect(optionsFactory.createMulterOptions.called).to.be.true; + }); + }); + }); +}); diff --git a/packages/platform-fastify/test/multer/multer/multer.utils.spec.ts b/packages/platform-fastify/test/multer/multer/multer.utils.spec.ts new file mode 100644 index 00000000000..7ad1eec9069 --- /dev/null +++ b/packages/platform-fastify/test/multer/multer/multer.utils.spec.ts @@ -0,0 +1,41 @@ +import { + BadRequestException, + HttpException, + PayloadTooLargeException, +} from '@nestjs/common'; +import { expect } from 'chai'; +import { multerExceptions } from '../../../multer/multer/multer.constants'; +import { transformException } from '../../../multer/multer/multer.utils'; + +describe('transformException', () => { + describe('if error does not exist', () => { + it('behave as identity', () => { + const err = undefined; + expect(transformException(err)).to.be.eq(err); + }); + }); + describe('if error is instance of HttpException', () => { + it('behave as identity', () => { + const err = new HttpException('response', 500); + expect(transformException(err)).to.be.eq(err); + }); + }); + describe('if error exists and is not instance of HttpException', () => { + describe('and is LIMIT_FILE_SIZE exception', () => { + it('should return "PayloadTooLargeException"', () => { + const err = { message: multerExceptions.LIMIT_FILE_SIZE }; + expect(transformException(err as any)).to.be.instanceof( + PayloadTooLargeException, + ); + }); + }); + describe('and is multer exception but not a LIMIT_FILE_SIZE', () => { + it('should return "BadRequestException"', () => { + const err = { message: multerExceptions.LIMIT_FIELD_KEY }; + expect(transformException(err as any)).to.be.instanceof( + BadRequestException, + ); + }); + }); + }); +});