From fbdafccbff538ccf528f22f83e8a9738d0a27918 Mon Sep 17 00:00:00 2001 From: richard483 Date: Sun, 3 Dec 2023 23:14:49 +0700 Subject: [PATCH 1/3] unify error response on auth controller --- src/app.module.ts | 10 ++- src/auth/auth.controller.ts | 30 ++----- src/auth/auth.service.ts | 15 +++- src/auth/test/auth.controller.spec.ts | 56 ++++++------ src/auth/test/auth.service.spec.ts | 15 ++-- src/interceptors/response.interceptor.ts | 103 +++++++++++++++++++++++ src/main.ts | 20 ++++- 7 files changed, 183 insertions(+), 66 deletions(-) create mode 100644 src/interceptors/response.interceptor.ts diff --git a/src/app.module.ts b/src/app.module.ts index a32927e..23caf97 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,6 +7,8 @@ import { ConfigModule } from '@nestjs/config'; import { JobModule } from './job/job.module'; import { ContractModule } from './contract/contract.module'; import { RatingModule } from './rating/rating.module'; +import { ResponseInterceptor } from './interceptors/response.interceptor'; +import { APP_INTERCEPTOR } from '@nestjs/core'; @Module({ imports: [ @@ -18,6 +20,12 @@ import { RatingModule } from './rating/rating.module'; RatingModule, ], controllers: [AppController], - providers: [AppService], + providers: [ + AppService, + { + provide: APP_INTERCEPTOR, + useClass: ResponseInterceptor, + }, + ], }) export class AppModule {} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 27525bf..867de97 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -26,25 +26,15 @@ export class AuthController { @Post('login') async signIn(@Res() res, @Body() authenticateDto: AuthenticateRequest) { console.info('#AuthLogin request incoming with: ', authenticateDto); - try { - const response = await this.authService.login(authenticateDto, res); - return res.status(HttpStatus.OK).json({ ...response }); - } catch (error) { - console.error('#AuthLogin error caused by: ', error); - return res.status(error.status).json({ error: error.message }); - } + const response = await this.authService.login(authenticateDto, res); + return res.status(HttpStatus.OK).json({ ...response }); } @Post('register') async signUp(@Res() res, @Body() request: RegisterRequest) { console.info('#AuthRegister request incoming with: ', request); - try { - const response = await this.authService.register(request); - return res.status(HttpStatus.OK).json({ ...response }); - } catch (error) { - console.error('#AuthRegister error caused by: ', error); - return res.status(error.status).json({ error: error.message }); - } + const response = await this.authService.register(request); + return res.status(HttpStatus.OK).json({ ...response }); } @Get('google') @@ -57,14 +47,9 @@ export class AuthController { @UseGuards(GoogleGuard) async googleAuthRedirect(@Request() req, @Res() res) { console.info('#AuthGoogleAuthRedirect google auth request incoming'); - try { - const response = await this.authService.googleLogin(req, res); - // TODO : redirect to frontend - return res.status(HttpStatus.OK).json({ ...response }); - } catch (error) { - console.error('#AutGoogleAuthRedirect error caused by: ', error); - return res.status(error.status).json({ error: error.message }); - } + const response = await this.authService.googleLogin(req, res); + // TODO : redirect to frontend + return res.status(HttpStatus.OK).json({ ...response }); } @ApiBearerAuth() @@ -73,6 +58,7 @@ export class AuthController { @UseGuards(JwtAuthGuard, RoleGuard) @Get('info') async getProfileInfo(@Request() req, @Res() res) { + console.info('#AuthGetProfileInfo request incoming'); return res.status(HttpStatus.OK).json({ ...req.user }); } } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 9dd2419..7f4453f 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,5 +1,6 @@ import { - BadRequestException, + HttpException, + HttpStatus, Injectable, UnauthorizedException, } from '@nestjs/common'; @@ -24,7 +25,7 @@ export class AuthService { ): Promise { const user = await this.usersService.findOne(authenticateRequest.email); if (!user) { - throw new UnauthorizedException('INVALID_CREDENTIALS'); + throw new HttpException({ user: 'NOT_FOUND' }, HttpStatus.NOT_FOUND); } const isPasswordMatch = await compare( @@ -33,7 +34,10 @@ export class AuthService { ); if (!isPasswordMatch) { - throw new UnauthorizedException('INVALID_CREDENTIALS'); + throw new HttpException( + { password: 'NOT_MATCH' }, + HttpStatus.BAD_REQUEST, + ); } // remove password from user object @@ -54,7 +58,10 @@ export class AuthService { async register(userCreate: RegisterRequest): Promise { if (!this.passwordValidation(userCreate.password)) { - throw new BadRequestException('INVALID_PASSWORD'); + throw new HttpException( + { password: 'INVALID_PASSWORD' }, + HttpStatus.BAD_REQUEST, + ); } const user = await this.usersService.create({ ...userCreate, diff --git a/src/auth/test/auth.controller.spec.ts b/src/auth/test/auth.controller.spec.ts index 26e32cc..71f515a 100644 --- a/src/auth/test/auth.controller.spec.ts +++ b/src/auth/test/auth.controller.spec.ts @@ -109,15 +109,15 @@ describe('AuthController', () => { .spyOn(authService, 'login') .mockRejectedValue(mockBadRequestResponse); - const response = await controller.signIn(mockRes, null); - - expect(response).toEqual(mockBadRequestResponse); - expect(statusSpy).toBeCalledWith(HttpStatus.BAD_REQUEST); - expect(jsonSpy).toBeCalledWith({ error: mockBadRequestResponse.message }); - expect(loginSpy).toBeCalledTimes(1); - statusSpy.mockRestore(); - jsonSpy.mockRestore(); - loginSpy.mockRestore(); + try { + await controller.signIn(mockRes, null); + } catch (e) { + expect(e).toEqual(mockBadRequestResponse); + expect(loginSpy).toBeCalledTimes(1); + statusSpy.mockRestore(); + jsonSpy.mockRestore(); + loginSpy.mockRestore(); + } }); it('register success', async () => { @@ -157,15 +157,15 @@ describe('AuthController', () => { .spyOn(authService, 'register') .mockRejectedValue(mockBadRequestResponse); - const response = await controller.signUp(mockRes, null); - - expect(response).toEqual(mockBadRequestResponse); - expect(statusSpy).toBeCalledWith(HttpStatus.BAD_REQUEST); - expect(jsonSpy).toBeCalledWith({ error: mockBadRequestResponse.message }); - expect(registerSpy).toBeCalledTimes(1); - statusSpy.mockRestore(); - jsonSpy.mockRestore(); - registerSpy.mockRestore(); + try { + await controller.signUp(mockRes, null); + } catch (e) { + expect(e).toEqual(mockBadRequestResponse); + expect(registerSpy).toBeCalledTimes(1); + statusSpy.mockRestore(); + jsonSpy.mockRestore(); + registerSpy.mockRestore(); + } }); it('getProfileInfo', async () => { @@ -222,17 +222,15 @@ describe('AuthController', () => { .spyOn(authService, 'googleLogin') .mockRejectedValue(mockBadRequestResponse); - const response = await controller.googleAuthRedirect(null, mockRes); - - expect(response).toEqual(mockBadRequestResponse); - expect(statusSpy).toBeCalledWith(HttpStatus.BAD_REQUEST); - expect(jsonSpy).toBeCalledWith({ - error: mockBadRequestResponse.message, - }); - expect(loginSpy).toBeCalledTimes(1); - statusSpy.mockRestore(); - jsonSpy.mockRestore(); - loginSpy.mockRestore(); + try { + await controller.googleAuthRedirect(null, mockRes); + } catch (e) { + expect(e).toEqual(mockBadRequestResponse); + expect(loginSpy).toBeCalledTimes(1); + statusSpy.mockRestore(); + jsonSpy.mockRestore(); + loginSpy.mockRestore(); + } }); it('google placeholder', async () => { diff --git a/src/auth/test/auth.service.spec.ts b/src/auth/test/auth.service.spec.ts index 935dda5..5e1e5fa 100644 --- a/src/auth/test/auth.service.spec.ts +++ b/src/auth/test/auth.service.spec.ts @@ -3,12 +3,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { AuthService } from '../auth.service'; import { UsersService } from '../../users/users.service'; -import { BadRequestException, UnauthorizedException } from '@nestjs/common'; +import { HttpException, UnauthorizedException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { IGoogleUser } from '../interface/auth.interface'; import { Role } from '../roles/role.enum'; import { compare, genSaltSync, hashSync } from 'bcrypt'; -import { mock } from 'node:test'; describe('AuthService', () => { let service: AuthService; @@ -100,8 +99,8 @@ describe('AuthService', () => { password: 'password', }); } catch (e) { - expect(e).toBeInstanceOf(UnauthorizedException); - expect(e.message).toBe('INVALID_CREDENTIALS'); + expect(e).toBeInstanceOf(HttpException); + expect(e.message).toBe('Http Exception'); } expect(findOneSpy).toBeCalledTimes(1); @@ -122,8 +121,8 @@ describe('AuthService', () => { password: 'password', }); } catch (e) { - expect(e).toBeInstanceOf(UnauthorizedException); - expect(e.message).toBe('INVALID_CREDENTIALS'); + expect(e).toBeInstanceOf(HttpException); + expect(e.message).toBe('Http Exception'); } expect(findOneSpy).toBeCalledTimes(1); @@ -188,8 +187,8 @@ describe('AuthService', () => { await service.register(mockRegisterRequest); } catch (e) { console.log('#register failed invalid password', e); - expect(e).toBeInstanceOf(BadRequestException); - expect(e.message).toBe('INVALID_PASSWORD'); + expect(e).toBeInstanceOf(HttpException); + expect(e.message).toBe('Http Exception'); } }); diff --git a/src/interceptors/response.interceptor.ts b/src/interceptors/response.interceptor.ts new file mode 100644 index 0000000..8e2e948 --- /dev/null +++ b/src/interceptors/response.interceptor.ts @@ -0,0 +1,103 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + HttpException, +} from '@nestjs/common'; +import { HttpErrorByCode } from '@nestjs/common/utils/http-error-by-code.util'; +import { Observable, throwError } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; + +@Injectable() +export class ResponseInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + map((res: unknown) => this.responseHandler(res, context)), + catchError((err: HttpException) => + throwError(() => this.errorHandler(err, context)), + ), + ); + } + + private errorHandler(exception: HttpException, context: ExecutionContext) { + const ctx = context.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + const status = + exception instanceof HttpException ? exception.getStatus() : 500; + + console.error( + '#Error response to path', + request.url, + 'with error: ', + exception.name, + 'and status: ', + status, + 'and exception message: ', + exception.message, + 'and stack trace: ', + exception.stack, + ); + console.error('#Error caused by: ', HttpErrorByCode[status]); + + response.status(status).json({ + status: false, + statusCode: status, + message: exception.getResponse(), + error: this.htttpCodeParser(status), + }); + } + + private responseHandler(res: any, context: ExecutionContext) { + const ctx = context.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + const statusCode = response.statusCode; + return { + status: true, + path: request.url, + statusCode, + result: res, + }; + } + + private htttpCodeParser(statusCode: number) { + switch (statusCode) { + case 200: + return 'OK'; + case 201: + return 'CREATED'; + case 202: + return 'ACCEPTED'; + case 204: + return 'NO_CONTENT'; + case 400: + return 'BAD_REQUEST'; + case 401: + return 'UNAUTHORIZED'; + case 403: + return 'FORBIDDEN'; + case 404: + return 'NOT_FOUND'; + case 406: + return 'NOT_ACCEPTABLE'; + case 409: + return 'CONFLICT'; + case 410: + return 'GONE'; + case 422: + return 'UNPROCESSABLE_ENTITY'; + case 500: + return 'INTERNAL_SERVER_ERROR'; + case 501: + return 'NOT_IMPLEMENTED'; + case 503: + return 'SERVICE_UNAVAILABLE'; + default: + return 'INTERNAL_SERVER_ERROR'; + } + } +} diff --git a/src/main.ts b/src/main.ts index 80c240e..e6783bd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,8 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { ValidationPipe } from '@nestjs/common'; +import { HttpException, HttpStatus, ValidationPipe } from '@nestjs/common'; +import { ResponseInterceptor } from './interceptors/response.interceptor'; declare const module: any; @@ -14,6 +15,8 @@ async function bootstrap() { methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], }); + app.useGlobalInterceptors(new ResponseInterceptor()); + const version = process.env.npm_package_version; const config = new DocumentBuilder() @@ -24,7 +27,20 @@ async function bootstrap() { .addCookieAuth('EToken') .build(); - app.useGlobalPipes(new ValidationPipe()); + app.useGlobalPipes( + new ValidationPipe({ + exceptionFactory: (errors) => { + const result = errors.map((error) => { + return { + ...error.constraints, + }; + }); + console.error('#Validation error caused by: ', errors); + return new HttpException(result, HttpStatus.BAD_REQUEST); + }, + stopAtFirstError: true, + }), + ); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('/api/swagger', app, document); From 435e2979f32ab715ffc14a77f9efb65865e3b04f Mon Sep 17 00:00:00 2001 From: richard483 Date: Mon, 4 Dec 2023 00:57:24 +0700 Subject: [PATCH 2/3] fixed ALL controller responses & exception, tested ALL api --- src/auth/auth.controller.ts | 9 ++- src/auth/auth.service.ts | 12 ++-- src/auth/jwt/jwt-auth.guard.ts | 15 +++- src/auth/test/auth.controller.spec.ts | 8 --- src/auth/test/auth.service.spec.ts | 6 +- src/contract/contract.controller.ts | 14 ++-- src/contract/temp/contract.temp.txt | 1 + src/contract/test/contract.controller.spec.ts | 40 ++++++----- src/interceptors/response.interceptor.ts | 19 +++--- src/job/job.controller.ts | 49 ++++--------- src/job/test/job.controller.spec.ts | 68 +++++++++++-------- src/rating/rating.controller.ts | 30 ++------ src/rating/test/rating.controller.spec.ts | 38 ++++++----- src/users/test/users.controller.spec.ts | 14 ++-- src/users/user.controller.ts | 33 ++------- 15 files changed, 154 insertions(+), 202 deletions(-) create mode 100644 src/contract/temp/contract.temp.txt diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 867de97..e3f4513 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -2,7 +2,6 @@ import { Body, Controller, Get, - HttpStatus, Post, Request, Res, @@ -27,14 +26,14 @@ export class AuthController { async signIn(@Res() res, @Body() authenticateDto: AuthenticateRequest) { console.info('#AuthLogin request incoming with: ', authenticateDto); const response = await this.authService.login(authenticateDto, res); - return res.status(HttpStatus.OK).json({ ...response }); + return response; } @Post('register') async signUp(@Res() res, @Body() request: RegisterRequest) { console.info('#AuthRegister request incoming with: ', request); const response = await this.authService.register(request); - return res.status(HttpStatus.OK).json({ ...response }); + return response; } @Get('google') @@ -49,7 +48,7 @@ export class AuthController { console.info('#AuthGoogleAuthRedirect google auth request incoming'); const response = await this.authService.googleLogin(req, res); // TODO : redirect to frontend - return res.status(HttpStatus.OK).json({ ...response }); + return response; } @ApiBearerAuth() @@ -59,6 +58,6 @@ export class AuthController { @Get('info') async getProfileInfo(@Request() req, @Res() res) { console.info('#AuthGetProfileInfo request incoming'); - return res.status(HttpStatus.OK).json({ ...req.user }); + return req.user; } } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 7f4453f..2236989 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,9 +1,4 @@ -import { - HttpException, - HttpStatus, - Injectable, - UnauthorizedException, -} from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { UsersService } from '../users/users.service'; import { AuthenticateRequest } from './requests/authenticate.request'; @@ -74,7 +69,10 @@ export class AuthService { const user: IGoogleUser = req.user; if (!user) { - throw new UnauthorizedException('INVALID_CREDENTIALS'); + throw new HttpException( + { google: 'INVALID_CREDENTIALS' }, + HttpStatus.BAD_REQUEST, + ); } let userDb: IUser = await this.usersService.findOneByEmail(user.email); diff --git a/src/auth/jwt/jwt-auth.guard.ts b/src/auth/jwt/jwt-auth.guard.ts index dd1a35b..4957405 100644 --- a/src/auth/jwt/jwt-auth.guard.ts +++ b/src/auth/jwt/jwt-auth.guard.ts @@ -1,4 +1,4 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() @@ -6,9 +6,18 @@ export class JwtAuthGuard extends AuthGuard('jwt') { handleRequest(err, user, info) { if (err || !user) { if (info && info.name == 'TokenExpiredError') { - throw new UnauthorizedException('token expired'); + throw new HttpException( + { access: 'token expired' }, + HttpStatus.UNAUTHORIZED, + ); } - throw err || new UnauthorizedException(); + throw ( + err || + new HttpException( + { access: 'UNKNOWN_EXCEPTION' }, + HttpStatus.UNAUTHORIZED, + ) + ); } return user; diff --git a/src/auth/test/auth.controller.spec.ts b/src/auth/test/auth.controller.spec.ts index 71f515a..703f67c 100644 --- a/src/auth/test/auth.controller.spec.ts +++ b/src/auth/test/auth.controller.spec.ts @@ -88,8 +88,6 @@ describe('AuthController', () => { const response = await controller.signIn(mockRes, null); expect(response).toEqual(mockIAuthSuccessResponse); - expect(statusSpy).toBeCalledWith(HttpStatus.OK); - expect(jsonSpy).toBeCalledWith(mockIAuthSuccessResponse); expect(loginSpy).toBeCalledTimes(1); statusSpy.mockRestore(); jsonSpy.mockRestore(); @@ -136,8 +134,6 @@ describe('AuthController', () => { const response = await controller.signUp(mockRes, null); expect(response).toEqual(mockIUserResponse); - expect(statusSpy).toBeCalledWith(HttpStatus.OK); - expect(jsonSpy).toBeCalledWith(mockIUserResponse); expect(registerSpy).toBeCalledTimes(1); statusSpy.mockRestore(); jsonSpy.mockRestore(); @@ -181,8 +177,6 @@ describe('AuthController', () => { const response = await controller.getProfileInfo(mockReq, mockRes); expect(response).toEqual(mockIUserResponse); - expect(statusSpy).toBeCalledWith(HttpStatus.OK); - expect(jsonSpy).toBeCalledWith(mockIUserResponse); }); it('googleRedirectLogin success', async () => { @@ -201,8 +195,6 @@ describe('AuthController', () => { const response = await controller.googleAuthRedirect(null, mockRes); expect(response).toEqual(mockIAuthSuccessResponse); - expect(statusSpy).toBeCalledWith(HttpStatus.OK); - expect(jsonSpy).toBeCalledWith(mockIAuthSuccessResponse); expect(googleLoginSpy).toBeCalledTimes(1); statusSpy.mockRestore(); jsonSpy.mockRestore(); diff --git a/src/auth/test/auth.service.spec.ts b/src/auth/test/auth.service.spec.ts index 5e1e5fa..f3c2e1f 100644 --- a/src/auth/test/auth.service.spec.ts +++ b/src/auth/test/auth.service.spec.ts @@ -3,7 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { AuthService } from '../auth.service'; import { UsersService } from '../../users/users.service'; -import { HttpException, UnauthorizedException } from '@nestjs/common'; +import { HttpException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { IGoogleUser } from '../interface/auth.interface'; import { Role } from '../roles/role.enum'; @@ -234,8 +234,8 @@ describe('AuthService', () => { try { await service.googleLogin(req, res); } catch (e) { - expect(e).toBeInstanceOf(UnauthorizedException); - expect(e.message).toBe('INVALID_CREDENTIALS'); + expect(e).toBeInstanceOf(HttpException); + expect(e.message).toBe('Http Exception'); } }); diff --git a/src/contract/contract.controller.ts b/src/contract/contract.controller.ts index 697e2f8..bdd966a 100644 --- a/src/contract/contract.controller.ts +++ b/src/contract/contract.controller.ts @@ -1,7 +1,6 @@ import { Body, Controller, - HttpStatus, Post, Get, Res, @@ -30,12 +29,9 @@ export class ContractController { @UseGuards(JwtAuthGuard, RoleGuard) @Post('create') async createContract(@Res() res, @Body() contract: ContractCreateDto) { - try { - const response = await this.contractService.create(contract); - return res.status(HttpStatus.OK).json({ response }); - } catch (error) { - return res.status(error.status).json({ error: error.message }); - } + console.info('#ContractCreate request incoming with: ', contract); + const response = await this.contractService.create(contract); + return response; } //9432dae7-ba04-410a-a4ff-27d6da87ae63 @@ -46,6 +42,7 @@ export class ContractController { @Res({ passthrough: true }) res, @Param() params: any, ) { + console.info('#ContractGenerate request incoming with: ', params); const contractId = params.contractId; try { await this.contractService.generate(contractId); @@ -53,9 +50,6 @@ export class ContractController { join(process.cwd(), '/src/contract/temp/', `${params.contractId}.pdf`), ); return await new StreamableFile(file); - } catch (error) { - console.error('#generateContract error caused by: ', error); - return res.status(error.status).json({ error: error.message }); } finally { this.contractService.removeFile(contractId); console.log( diff --git a/src/contract/temp/contract.temp.txt b/src/contract/temp/contract.temp.txt new file mode 100644 index 0000000..4ec3196 --- /dev/null +++ b/src/contract/temp/contract.temp.txt @@ -0,0 +1 @@ +DO NOT DELETE THIS FILE \ No newline at end of file diff --git a/src/contract/test/contract.controller.spec.ts b/src/contract/test/contract.controller.spec.ts index 8c08f0c..e3c6463 100644 --- a/src/contract/test/contract.controller.spec.ts +++ b/src/contract/test/contract.controller.spec.ts @@ -95,12 +95,14 @@ describe('ContractController', () => { json: jsonSpy, }; - const res = await controller.createContract(mockRes, null); - - expect(createSpy).toBeCalledWith(null); - expect(res).toEqual(mockResponse); - - createSpy.mockRestore(); + try { + await controller.createContract(mockRes, null); + } catch (e) { + expect(createSpy).toBeCalledWith(null); + expect(e).toEqual(mockResponse); + + createSpy.mockRestore(); + } }); it('generateContract success', async () => { @@ -120,17 +122,19 @@ describe('ContractController', () => { json: jsonSpy, }; - await controller.generateContract(mockRes, { - contractId: 'randomId', - }); - - expect(generateSpy).toBeCalledWith('randomId'); - expect(createReadStreamSpy).toBeCalledWith( - process.cwd() + '/src/contract/temp/' + `randomId.pdf`, - ); - - generateSpy.mockRestore(); - createReadStreamSpy.mockRestore(); - joinSpy.mockRestore(); + try { + await controller.generateContract(mockRes, { + contractId: 'randomId', + }); + } catch (e) { + expect(generateSpy).toBeCalledWith('randomId'); + expect(createReadStreamSpy).toBeCalledWith( + process.cwd() + '/src/contract/temp/' + `randomId.pdf`, + ); + + generateSpy.mockRestore(); + createReadStreamSpy.mockRestore(); + joinSpy.mockRestore(); + } }); }); diff --git a/src/interceptors/response.interceptor.ts b/src/interceptors/response.interceptor.ts index 8e2e948..84c901e 100644 --- a/src/interceptors/response.interceptor.ts +++ b/src/interceptors/response.interceptor.ts @@ -4,8 +4,9 @@ import { ExecutionContext, CallHandler, HttpException, + HttpStatus, + StreamableFile, } from '@nestjs/common'; -import { HttpErrorByCode } from '@nestjs/common/utils/http-error-by-code.util'; import { Observable, throwError } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; @@ -40,7 +41,6 @@ export class ResponseInterceptor implements NestInterceptor { 'and stack trace: ', exception.stack, ); - console.error('#Error caused by: ', HttpErrorByCode[status]); response.status(status).json({ status: false, @@ -51,17 +51,18 @@ export class ResponseInterceptor implements NestInterceptor { } private responseHandler(res: any, context: ExecutionContext) { + if (res instanceof StreamableFile) { + return res; + } + const ctx = context.switchToHttp(); const response = ctx.getResponse(); - const request = ctx.getRequest(); - const statusCode = response.statusCode; - return { + response.status(HttpStatus.OK).json({ status: true, - path: request.url, - statusCode, - result: res, - }; + statusCode: HttpStatus.OK, + data: res, + }); } private htttpCodeParser(statusCode: number) { diff --git a/src/job/job.controller.ts b/src/job/job.controller.ts index b6ee449..3e6c62a 100644 --- a/src/job/job.controller.ts +++ b/src/job/job.controller.ts @@ -2,7 +2,6 @@ import { Body, Controller, Get, - HttpStatus, Param, Post, Res, @@ -27,16 +26,11 @@ export class JobController { @UseGuards(JwtAuthGuard, RoleGuard) @Post('create') async createJob(@Res() res, @Body() job: JobCreateDto) { - try { - console.log( - `#createJob request incoming with res: ${res} and data: ${job}`, - ); - const response = await this.jobService.create(job); - return res.status(HttpStatus.OK).json({ response }); - } catch (error) { - console.error('#createJob error caused by: ', error); - return res.status(error.status).json({ error: error.message }); - } + console.log( + `#createJob request incoming with res: ${res} and data: ${job}`, + ); + const response = await this.jobService.create(job); + return response; } @ApiBearerAuth() @@ -45,16 +39,11 @@ export class JobController { @UseGuards(JwtAuthGuard, RoleGuard) @Get('delete/:jobId') async deleteJob(@Res() res, @Param() params: any) { - try { - console.log( - `#deleteJob request incoming with res: ${res} and params: ${params}`, - ); - const response = await this.jobService.delete(params.jobId); - return res.status(HttpStatus.OK).json({ response }); - } catch (error) { - console.error('#deleteJob error caused by: ', error); - return res.status(error.status).json({ error: error.message }); - } + console.log( + `#deleteJob request incoming with res: ${res} and params: ${params}`, + ); + const response = await this.jobService.delete(params.jobId); + return response; } @ApiBearerAuth() @@ -62,13 +51,8 @@ export class JobController { @UseGuards(JwtAuthGuard, RoleGuard) @Post('update') async updateJob(@Res() res, @Body() job: JobUpdateDto) { - try { - const response = await this.jobService.update(job); - return res.status(HttpStatus.OK).json({ response }); - } catch (error) { - console.error('#updateJob error caused by: ', error); - return res.status(error.status).json({ error: error.message }); - } + const response = await this.jobService.update(job); + return response; } @ApiBearerAuth() @@ -77,12 +61,7 @@ export class JobController { @UseGuards(JwtAuthGuard, RoleGuard) @Get('/:jobId') async getjob(@Res() res, @Param() params: any) { - try { - const response = await this.jobService.getById(params.jobId); - return res.status(HttpStatus.OK).json({ response }); - } catch (error) { - console.error('#getJob error caused by: ', error); - return res.status(error.status).json({ error: error.message }); - } + const response = await this.jobService.getById(params.jobId); + return response; } } diff --git a/src/job/test/job.controller.spec.ts b/src/job/test/job.controller.spec.ts index 2c213ab..9620c28 100644 --- a/src/job/test/job.controller.spec.ts +++ b/src/job/test/job.controller.spec.ts @@ -89,12 +89,14 @@ describe('JobController', () => { json: jsonSpy, }; - const res = await controller.createJob(mockRes, null); - - expect(createSpy).toBeCalledWith(null); - expect(res).toEqual(mockResponse); - - createSpy.mockRestore(); + try { + await controller.createJob(mockRes, null); + } catch (e) { + expect(createSpy).toBeCalledWith(null); + expect(e).toEqual(mockResponse); + + createSpy.mockRestore(); + } }); it('deleteJob success', async () => { @@ -146,14 +148,16 @@ describe('JobController', () => { json: jsonSpy, }; - const res = await controller.deleteJob(mockRes, { - jobId: null, - }); + try { + await controller.deleteJob(mockRes, { + jobId: null, + }); + } catch (e) { + expect(deleteSpy).toBeCalledWith(null); + expect(e).toEqual(mockResponse); - expect(deleteSpy).toBeCalledWith(null); - expect(res).toEqual(mockResponse); - - deleteSpy.mockRestore(); + deleteSpy.mockRestore(); + } }); it('updateJob success', async () => { const mockJob: IJob = { @@ -206,16 +210,18 @@ describe('JobController', () => { json: jsonSpy, }; - const res = await controller.updateJob(mockRes, { - id: null, - }); - - expect(updateSpy).toBeCalledWith({ - id: null, - }); - expect(res).toEqual(mockResponse); - - updateSpy.mockRestore(); + try { + await controller.updateJob(mockRes, { + id: null, + }); + } catch (e) { + expect(updateSpy).toBeCalledWith({ + id: null, + }); + expect(e).toEqual(mockResponse); + + updateSpy.mockRestore(); + } }); it('getJob success', async () => { const mockJob: IJob = { @@ -266,13 +272,15 @@ describe('JobController', () => { json: jsonSpy, }; - const res = await controller.getjob(mockRes, { - jobId: null, - }); + try { + await controller.getjob(mockRes, { + jobId: null, + }); + } catch (e) { + expect(getJobSpy).toBeCalledWith(null); + expect(e).toEqual(mockResponse); - expect(getJobSpy).toBeCalledWith(null); - expect(res).toEqual(mockResponse); - - getJobSpy.mockRestore(); + getJobSpy.mockRestore(); + } }); }); diff --git a/src/rating/rating.controller.ts b/src/rating/rating.controller.ts index 02206bd..5c951a7 100644 --- a/src/rating/rating.controller.ts +++ b/src/rating/rating.controller.ts @@ -1,11 +1,4 @@ -import { - Body, - Controller, - HttpStatus, - Post, - Res, - UseGuards, -} from '@nestjs/common'; +import { Body, Controller, Post, Res, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { RatingService } from './rating.service'; import { RatingCreateDto } from './dto/rating-create.dto'; @@ -25,16 +18,8 @@ export class RatingController { @UseGuards(JwtAuthGuard, RoleGuard) @Post('create') async createRating(@Res() res, @Body() data: RatingCreateDto) { - try { - console.log( - `#createRating request incoming with res: ${res} and data: ${data}`, - ); - const response = await this.ratingService.create(data); - return res.status(HttpStatus.OK).json({ response }); - } catch (error) { - console.error('#createJob error caused by: ', error); - return res.status(error.status).json({ error: error.message }); - } + const response = await this.ratingService.create(data); + return response; } @ApiBearerAuth() @@ -42,12 +27,7 @@ export class RatingController { @UseGuards(JwtAuthGuard, RoleGuard) @Post('update') async updateRating(@Res() res, @Body() data: RatingUpdateDto) { - try { - const response = await this.ratingService.update(data); - return res.status(HttpStatus.OK).json({ response }); - } catch (error) { - console.error('#updateJob error caused by: ', error); - return res.status(error.status).json({ error: error.message }); - } + const response = await this.ratingService.update(data); + return response; } } diff --git a/src/rating/test/rating.controller.spec.ts b/src/rating/test/rating.controller.spec.ts index f601603..7146bdd 100644 --- a/src/rating/test/rating.controller.spec.ts +++ b/src/rating/test/rating.controller.spec.ts @@ -88,12 +88,14 @@ describe('RatingController', () => { json: jsonSpy, }; - const res = await controller.createRating(mockRes, null); - - expect(createSpy).toBeCalledWith(null); - expect(res).toEqual(mockResponse); - - createSpy.mockRestore(); + try { + await controller.createRating(mockRes, null); + } catch (e) { + expect(createSpy).toBeCalledWith(null); + expect(e).toEqual(mockResponse); + + createSpy.mockRestore(); + } }); it('updateRating success', async () => { const ratingUpdateDto: RatingUpdateDto = { @@ -156,16 +158,18 @@ describe('RatingController', () => { json: jsonSpy, }; - const res = await controller.updateRating(mockRes, ratingUpdateDto); - - expect(updateSpy).toBeCalledWith({ - id: 'randomId', - userId: 'test', - givenByUserId: 'test', - ratingOf10: 9, - }); - expect(res).toEqual(mockResponse); - - updateSpy.mockRestore(); + try { + await controller.updateRating(mockRes, ratingUpdateDto); + } catch (e) { + expect(updateSpy).toBeCalledWith({ + id: 'randomId', + userId: 'test', + givenByUserId: 'test', + ratingOf10: 9, + }); + expect(e).toEqual(mockResponse); + + updateSpy.mockRestore(); + } }); }); diff --git a/src/users/test/users.controller.spec.ts b/src/users/test/users.controller.spec.ts index 3e9b613..ac78cf3 100644 --- a/src/users/test/users.controller.spec.ts +++ b/src/users/test/users.controller.spec.ts @@ -96,11 +96,13 @@ describe('AuthController', () => { json: jsonSpy, }; - const res = await controller.createAdmin(mockRes, null); - - expect(createSpy).toBeCalledWith(null); - expect(res).toEqual(mockResponse); - - createSpy.mockRestore(); + try { + await controller.createAdmin(mockRes, null); + } catch (e) { + expect(createSpy).toBeCalledWith(null); + expect(e).toEqual(mockResponse); + + createSpy.mockRestore(); + } }); }); diff --git a/src/users/user.controller.ts b/src/users/user.controller.ts index ba6aeb2..842a3b5 100644 --- a/src/users/user.controller.ts +++ b/src/users/user.controller.ts @@ -1,11 +1,4 @@ -import { - Body, - Controller, - HttpStatus, - Post, - Res, - UseGuards, -} from '@nestjs/common'; +import { Body, Controller, Post, Res, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { UsersService } from './users.service'; import { Roles } from '../auth/roles/role.decorator'; @@ -40,12 +33,8 @@ export class UserController { @UseGuards(JwtAuthGuard, RoleGuard) @Post('create') async createAdmin(@Res() res, @Body() user: UserCreateRequest) { - try { - const response = await this.userService.create(user); - return res.status(HttpStatus.OK).json({ response }); - } catch (error) { - return res.status(error.status).json({ error: error.message }); - } + const response = await this.userService.create(user); + return response; } @ApiBearerAuth() @@ -53,12 +42,8 @@ export class UserController { @UseGuards(JwtAuthGuard, RoleGuard) @Post('update') async update(@Res() res, @Body() data: UserUpdateRequest) { - try { - const response = await this.userService.update(data); - return res.status(HttpStatus.OK).json({ response }); - } catch (error) { - return res.status(error.status).json({ error: error.message }); - } + const response = await this.userService.update(data); + return response; } @ApiBearerAuth() @@ -66,11 +51,7 @@ export class UserController { @UseGuards(JwtAuthGuard, RoleGuard) @Post('filter') async filterUser(@Res() res, @Body() body: UserFilterRequest) { - try { - const response = await this.userService.findManyByList(body); - return res.status(HttpStatus.OK).json({ response }); - } catch (error) { - return res.status(error.status).json({ error: error.message }); - } + const response = await this.userService.findManyByList(body); + return response; } } From ca5b6536a7f060af99a85f0cf19ff101dadbff8d Mon Sep 17 00:00:00 2001 From: richard483 Date: Mon, 4 Dec 2023 01:52:29 +0700 Subject: [PATCH 3/3] tidy up error response & update docs --- docs/auth-controller.md | 67 ++++++++++++++---------- src/interceptors/response.interceptor.ts | 8 ++- src/main.ts | 2 +- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/docs/auth-controller.md b/docs/auth-controller.md index dca7e7b..1f4daf0 100644 --- a/docs/auth-controller.md +++ b/docs/auth-controller.md @@ -108,30 +108,27 @@ HTTP Code: 200 ```json { - "id": "89edfeb7-0b26-4c88-950a-2216123ec367", - "email": "asdasdas@gmail.com", - "username": "string", - "firstName": "string", - "lastName": "string", - "password": "$2b$10$wxulL2C86NpVTzWMlA9Knu9CVUdasWCwSwaC3tqj/S9ziA/iT9JiW", - "createdAt": "2023-11-22T17:19:18.472Z", - "updatedAt": "2023-11-22T17:19:18.472Z", - "roles": ["USER"], - "description": null, - "previousWorkplaceId": [], - "previousWorkplaceCount": null, - "ratingsAvg": null, - "hasGoogleAccount": false -} -``` - -### Password not match - -HTTP Code: 400 - -```json -{ - "error": "PASSWORD_NOT_MATCH" + "status": true, + "statusCode": 200, + "data": { + "id": "57b3634c-3789-4a46-b6f0-afe22a195f27", + "email": "aassaad@gmail.com", + "username": "username", + "firstName": "firsl", + "lastName": "last", + "password": "$2b$10$yAmMDeIyOHRo/bKfWWQ6N.Z/LrnU1PEA4lMBO/4M/48c23zz54QYa", + "createdAt": "2023-12-03T17:59:31.655Z", + "updatedAt": "2023-12-03T17:59:31.655Z", + "roles": [ + "USER" + ], + "description": null, + "previousWorkplaceId": [], + "previousWorkplaceCount": null, + "ratingsAvg": null, + "companyId": null, + "hasGoogleAccount": false + } } ``` @@ -141,7 +138,10 @@ HTTP Code: 400 ```json { - "error": "EMAIL_ALREADY_USED" + "status": false, + "statusCode": 400, + "message": "EMAIL_ALREADY_USED", + "error": "BAD_REQUEST" } ``` @@ -151,9 +151,14 @@ HTTP Code: 400 ```json { + "status": false, "statusCode": 400, - "message": ["email must be an email"], - "error": "Bad Request" + "message": [ + { + "isEmail": "email must be an email" + } + ], + "error": "BAD_REQUEST" } ``` @@ -163,8 +168,12 @@ HTTP Code: 400 ```json { + "status": false, "statusCode": 400, - "message": ["firstName must be a string", "firstName should not be empty"], - "error": "Bad Request" + "message": { + "firstName": "firstName must be a string", + "lastName": "lastName must be a string" + }, + "error": "BAD_REQUEST" } ``` diff --git a/src/interceptors/response.interceptor.ts b/src/interceptors/response.interceptor.ts index 84c901e..409d883 100644 --- a/src/interceptors/response.interceptor.ts +++ b/src/interceptors/response.interceptor.ts @@ -45,7 +45,13 @@ export class ResponseInterceptor implements NestInterceptor { response.status(status).json({ status: false, statusCode: status, - message: exception.getResponse(), + message: + exception.getResponse() instanceof Object + ? Object(exception.getResponse()).reduce( + (acc, obj) => ({ ...Object(acc), ...Object(obj) }), + {}, + ) + : exception.getResponse(), error: this.htttpCodeParser(status), }); } diff --git a/src/main.ts b/src/main.ts index e6783bd..319b5c3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -32,7 +32,7 @@ async function bootstrap() { exceptionFactory: (errors) => { const result = errors.map((error) => { return { - ...error.constraints, + [error.property]: Object.values(error.constraints)[0], }; }); console.error('#Validation error caused by: ', errors);