From 690a1cedff19197997fac6ca1f67e87032ae392f Mon Sep 17 00:00:00 2001 From: sahachide Date: Sat, 24 Apr 2021 14:15:09 +0200 Subject: [PATCH] feat: request body validation --- src/http/Context.ts | 35 ++++++++++++++++++- src/http/IncomingRequest.ts | 29 ++++++--------- src/types/interfaces.ts | 22 +++++++----- src/types/types.ts | 2 +- .../src/controller/RequestTestController.ts | 28 ++++++++++++++- test/http/request.test.ts | 28 +++++++++++++++ 6 files changed, 114 insertions(+), 30 deletions(-) diff --git a/src/http/Context.ts b/src/http/Context.ts index 50f6675..9b57dd2 100644 --- a/src/http/Context.ts +++ b/src/http/Context.ts @@ -1,5 +1,11 @@ import type { IncomingMessage, ServerResponse } from 'http' -import type { IncomingParams, ParsedBody, QueryString } from '../types/interfaces' +import type { + IncomingParams, + ParsedBody, + QueryString, + RequestValidationError, + Route, +} from '../types/interfaces' import { BodyParser } from './BodyParser' import { Cookie } from './Cookie' @@ -17,12 +23,15 @@ export class Context { res: Response error: ResponseError } + private requestBodyValidationErrors: RequestValidationError[] = [] private isBuild: boolean = false + private isReqBodyValid: boolean = true public async build( request: IncomingMessage, response: ServerResponse, params: IncomingParams, + route: Route, ): Promise { if (this.isBuild) { return @@ -39,6 +48,22 @@ export class Context { body = await bodyParser.parse(request) } + if (typeof route.validationSchema !== 'undefined') { + const validationResult = route.validationSchema.validate(body.fields) + + if (validationResult.error) { + this.isReqBodyValid = false + this.requestBodyValidationErrors = validationResult.error.details.map((error) => { + return { + message: error.message, + path: error.path, + } + }) + } else { + body.fields = validationResult.value as JsonObject + } + } + const cookie = config.web?.cookie?.enable ? new Cookie(request.headers) : null const req = new Request(request, body, params) const res = new Response(response, req, cookie) @@ -88,4 +113,12 @@ export class Context { get error(): ResponseError { return this.container.error } + + get isValid(): boolean { + return this.isReqBodyValid + } + + get validationErrors(): RequestValidationError[] { + return this.requestBodyValidationErrors + } } diff --git a/src/http/IncomingRequest.ts b/src/http/IncomingRequest.ts index 34c2ee7..1ffa8b7 100644 --- a/src/http/IncomingRequest.ts +++ b/src/http/IncomingRequest.ts @@ -17,7 +17,7 @@ export class IncomingRequest { requestConfig: RequestConfig, securityProviders: SecurityProviders, ): Promise { - const context = await this.buildContext(req, res, params) + const context = await this.buildContext(req, res, params, route) const authentication = await this.authenticate(route.authProvider, context, securityProviders) if (authentication.isAuth) { @@ -29,11 +29,15 @@ export class IncomingRequest { } } - await this.validate(context, route) + if (context.isValid) { + const handler = factory.build(context, requestConfig, route) - const handler = factory.build(context, requestConfig, route) - - await handler.run() + await handler.run() + } else { + context.error.badData('Bad Data', { + errors: context.validationErrors, + }) + } } else if (authentication.securityProvider) { await authentication.securityProvider.forbidden(context) } else { @@ -45,26 +49,15 @@ export class IncomingRequest { req: IncomingMessage, res: ServerResponse, params: IncomingParams, + route: Route, ): Promise { const context = new Context() - await context.build(req, res, params) + await context.build(req, res, params, route) return context } - protected async validate(context: Context, route: Route): Promise { - if (typeof route.validationSchema === 'undefined') { - return - } - - const test = route.validationSchema.validate(context.req.body) - // console.log(context.req.body) - - // console.log(test) - // console.log(test.error.details) - } - protected async authenticate( authProvider: unknown, context: Context, diff --git a/src/types/interfaces.ts b/src/types/interfaces.ts index 2c1f4be..a118e70 100644 --- a/src/types/interfaces.ts +++ b/src/types/interfaces.ts @@ -69,15 +69,6 @@ export interface ControllerDeclaration { module: Class routes: Route[] } - -export interface Route { - method: HTTPMethod - path: string - controllerMethod?: string - authProvider?: string - validationSchema?: ValidationSchema -} - export interface CommonJSZenModule { [key: string]: Class } @@ -249,6 +240,11 @@ export interface RegistryFactories { email: EmailFactory } +export interface RequestValidationError { + message: string + path: (string | number)[] +} + export interface RequestConfigController { type: REQUEST_TYPE.CONTROLLER controllerKey: string @@ -269,6 +265,14 @@ export interface RequestConfigSecurity { provider: SecurityProvider } +export interface Route { + method: HTTPMethod + path: string + controllerMethod?: string + authProvider?: string + validationSchema?: ValidationSchema +} + // ---- S export interface SecurityProviderAuthorizeResponse { diff --git a/src/types/types.ts b/src/types/types.ts index 40d6b74..62955a7 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -48,7 +48,7 @@ export type Email = EmailFactory export type EmailTemplates = Map -export type ErrorResponseData = JsonObject | JsonArray +export type ErrorResponseData = Record | JsonArray // ---- F // ---- G diff --git a/test/fixtures/testapp/src/controller/RequestTestController.ts b/test/fixtures/testapp/src/controller/RequestTestController.ts index c746082..bec9f3b 100644 --- a/test/fixtures/testapp/src/controller/RequestTestController.ts +++ b/test/fixtures/testapp/src/controller/RequestTestController.ts @@ -1,4 +1,16 @@ -import { Context, Controller, get, params, query, request, Request } from '../../../../../src' +import { + Context, + Controller, + get, + params, + post, + query, + request, + Request, + validate, + validation, + body, +} from '../../../../../src' import type { QueryString } from '../../../../../src/types/interfaces' @@ -71,4 +83,18 @@ export default class extends Controller { setterTests, } } + + @post('/request-test-validate') + @validation( + validate.object({ + foo: validate.string().required().alphanum().min(3).max(30), + bar: validate.string().required().alphanum().min(3).max(30), + }), + ) + public validationTest( + @body + { foo, bar }: { foo: string; bar: string }, + ) { + return { foo, bar } + } } diff --git a/test/http/request.test.ts b/test/http/request.test.ts index a02f28b..96a2d25 100644 --- a/test/http/request.test.ts +++ b/test/http/request.test.ts @@ -130,4 +130,32 @@ describe('Request', () => { accept: 'application/json', }) }) + + it('validates a POST request body', async () => { + const body = { + foo: 'bar', + bar: 'baz', + } + + await supertest(app.nodeServer) + .post('/request-test-validate') + .send(body) + .set('Accept', 'application/json') + .expect(201) + .expect('Content-Type', /json/) + .expect(body) + }) + + it('validates a POST request body (fail)', async () => { + const body = { + foo: 'bar', + } + + await supertest(app.nodeServer) + .post('/request-test-validate') + .send(body) + .set('Accept', 'application/json') + .expect(422) + .expect('Content-Type', /json/) + }) })