/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable unused-imports/no-unused-vars-ts */ import { Body, Controller, Get, Global, INestApplication, Module, Patch, Post, Put, Query, } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import * as Joi from 'joi'; import { JoiSchema } from 'joi-class-decorators'; import { JoiPipe, JOIPIPE_OPTIONS, JoiPipeModule, JoiValidationGroups } from 'nestjs-joi'; import * as request from 'supertest'; describe('NestJS integration', () => { class metatype { @JoiSchema(Joi.string().required()) @JoiSchema([JoiValidationGroups.CREATE], Joi.number().required()) @JoiSchema([JoiValidationGroups.UPDATE], Joi.array().required()) prop!: unknown; } let controller; let module: TestingModule; let app: INestApplication; let request_: request.Test; // Note: we will test one case (@Query or @Body) each, since the other cases // are considered to be equivalent describe('app module', () => { describe('without options', () => { const CASES = [JoiPipeModule, JoiPipeModule.forRoot()]; for (const pipeModule of CASES) { beforeEach(async () => { @Controller() class ctrl { @Get('/') get(@Query() query: metatype): unknown { return { status: 'OK' }; } @Post('/') post(@Body() body: metatype): unknown { return { status: 'OK' }; } @Put('/') put(@Body() body: metatype): unknown { return { status: 'OK' }; } @Patch('/') patch(@Body() body: metatype): unknown { return { status: 'OK' }; } } controller = ctrl; module = await Test.createTestingModule({ controllers: [controller], imports: [pipeModule], }).compile(); app = module.createNestApplication(); await app.init(); }); afterEach(async () => { await app.close(); }); describe('GET', () => { it('should use the pipe correctly (positive test)', async () => { return request(app.getHttpServer()) .get('/?prop=foo') .expect(res => expect(res.body).toEqual({ status: 'OK', }), ); }); it('should use the pipe correctly (negative test)', async () => { return request(app.getHttpServer()) .get('/') .expect(res => expect(res.body).toMatchObject({ statusCode: 400, message: expect.stringContaining('"prop" is required'), }), ); }); }); describe('POST', () => { it('should use the pipe correctly (positive test)', async () => { return request(app.getHttpServer()) .post('/') .send({ prop: 1 }) .expect(res => expect(res.body).toEqual({ status: 'OK', }), ); }); it('should use the pipe correctly (negative test)', async () => { return request(app.getHttpServer()) .post('/') .send({ prop: 'a' }) .expect(res => expect(res.body).toMatchObject({ statusCode: 400, message: expect.stringContaining('"prop" must be a number'), }), ); }); }); describe('PUT', () => { it('should use the pipe correctly (positive test)', async () => { return request(app.getHttpServer()) .put('/') .send({ prop: [] }) .expect(res => expect(res.body).toEqual({ status: 'OK', }), ); }); it('should use the pipe correctly (negative test)', async () => { return request(app.getHttpServer()) .put('/') .send({ prop: 'a' }) .expect(res => expect(res.body).toMatchObject({ statusCode: 400, message: expect.stringContaining('"prop" must be an array'), }), ); }); }); describe('PATCH', () => { it('should use the pipe correctly (positive test)', async () => { return request(app.getHttpServer()) .patch('/') .send({ prop: [] }) .expect(res => expect(res.body).toEqual({ status: 'OK', }), ); }); it('should use the pipe correctly (negative test)', async () => { return request(app.getHttpServer()) .patch('/') .send({ prop: 'a' }) .expect(res => expect(res.body).toMatchObject({ statusCode: 400, message: expect.stringContaining('"prop" must be an array'), }), ); }); }); } }); describe('with options in .forRoot()', () => { beforeEach(async () => { @Controller() class ctrl { @Get('/') get(@Query() query: metatype): unknown { return { status: 'OK' }; } } controller = ctrl; module = await Test.createTestingModule({ controllers: [controller], imports: [ JoiPipeModule.forRoot({ pipeOpts: { usePipeValidationException: true, }, }), ], }).compile(); app = module.createNestApplication(); // Silence the ExceptionHandler output on 500 app.useLogger(false); await app.init(); }); afterEach(async () => { await app.close(); }); it('should use the pipe correctly (positive test)', async () => { return request(app.getHttpServer()) .get('/?prop=foo') .expect(res => expect(res.body).toEqual({ status: 'OK', }), ); }); it('should use the pipe correctly (negative test)', async () => { return request(app.getHttpServer()) .get('/') .expect(res => expect(res.body).toMatchObject({ statusCode: 500, message: expect.stringContaining('Internal server error'), }), ); }); }); describe('options injection', () => { beforeEach(async () => { @Controller() class ctrl { @Get('/') get(@Query() query: metatype): unknown { return { status: 'OK' }; } } controller = ctrl; @Global() @Module({ providers: [ { provide: JOIPIPE_OPTIONS, useValue: { usePipeValidationException: true, }, }, ], exports: [JOIPIPE_OPTIONS], }) class OptionsModule {} module = await Test.createTestingModule({ controllers: [controller], imports: [OptionsModule, JoiPipeModule], }).compile(); app = module.createNestApplication(); // Silence the ExceptionHandler output on 500 app.useLogger(false); await app.init(); }); afterEach(async () => { await app.close(); }); it('should use the pipe correctly (positive test)', async () => { return request(app.getHttpServer()) .get('/?prop=foo') .expect(res => expect(res.body).toEqual({ status: 'OK', }), ); }); it('should use the pipe correctly (negative test)', async () => { return request(app.getHttpServer()) .get('/') .expect(res => expect(res.body).toMatchObject({ statusCode: 500, message: expect.stringContaining('Internal server error'), }), ); }); }); }); describe('method parameter pipe', () => { describe('without options', () => { beforeEach(async () => { @Controller() class ctrl { @Get('/') get(@Query(JoiPipe) query: metatype): unknown { return { status: 'OK' }; } } controller = ctrl; module = await Test.createTestingModule({ controllers: [controller], }).compile(); app = module.createNestApplication(); await app.init(); }); afterEach(async () => { await app.close(); }); it('should use the pipe correctly (positive test)', async () => { return request(app.getHttpServer()) .get('/?prop=foo') .expect(res => expect(res.body).toEqual({ status: 'OK', }), ); }); it('should use the pipe correctly (negative test)', async () => { return request(app.getHttpServer()) .get('/?prop=') .expect(res => expect(res.body).toMatchObject({ statusCode: 400, message: expect.stringContaining('"prop" is not allowed to be empty'), }), ); }); }); }); describe('options injection', () => { beforeEach(async () => { @Controller() class ctrl { @Get('/') get(@Query(JoiPipe) query: metatype): unknown { return { status: 'OK' }; } } controller = ctrl; module = await Test.createTestingModule({ controllers: [controller], providers: [ { provide: JOIPIPE_OPTIONS, useValue: { usePipeValidationException: true, }, }, ], }).compile(); app = module.createNestApplication(); // Silence the ExceptionHandler output on 500 app.useLogger(false); await app.init(); }); afterEach(async () => { await app.close(); }); it('should use the pipe correctly (positive test)', async () => { return request(app.getHttpServer()) .get('/?prop=foo') .expect(res => expect(res.body).toEqual({ status: 'OK', }), ); }); it('should use the pipe correctly (negative test)', async () => { return request(app.getHttpServer()) .get('/?prop=') .expect(res => expect(res.body).toMatchObject({ statusCode: 500, message: expect.stringContaining('Internal server error'), }), ); }); }); });