From f2607a05a27095872126194ce68643f30e7be02e Mon Sep 17 00:00:00 2001 From: jannyHou Date: Thu, 5 Jul 2018 16:57:15 -0400 Subject: [PATCH] feat: add tests for array and object --- packages/rest/src/index.ts | 2 +- .../src/{coercion => }/rest-http-error.ts | 0 .../src/validation/request-body.validator.ts | 75 +++++++----- .../test/unit/request-body.validator.test.ts | 113 +++++++++++++++++- 4 files changed, 154 insertions(+), 36 deletions(-) rename packages/rest/src/{coercion => }/rest-http-error.ts (100%) diff --git a/packages/rest/src/index.ts b/packages/rest/src/index.ts index de144469078d..5fec39d1d20f 100644 --- a/packages/rest/src/index.ts +++ b/packages/rest/src/index.ts @@ -30,7 +30,7 @@ export * from './rest.application'; export * from './rest.component'; export * from './rest.server'; export * from './sequence'; -export * from './coercion/rest-http-error'; +export * from './rest-http-error'; // export all errors from external http-errors package import * as HttpErrors from 'http-errors'; diff --git a/packages/rest/src/coercion/rest-http-error.ts b/packages/rest/src/rest-http-error.ts similarity index 100% rename from packages/rest/src/coercion/rest-http-error.ts rename to packages/rest/src/rest-http-error.ts diff --git a/packages/rest/src/validation/request-body.validator.ts b/packages/rest/src/validation/request-body.validator.ts index e934b9b63b1c..663d90e9605e 100644 --- a/packages/rest/src/validation/request-body.validator.ts +++ b/packages/rest/src/validation/request-body.validator.ts @@ -7,18 +7,26 @@ import { RequestBodyObject, SchemaObject, SchemasObject, - isReferenceObject, } from '@loopback/openapi-v3-types'; import * as AJV from 'ajv'; import * as debugModule from 'debug'; import * as util from 'util'; import {HttpErrors} from '..'; -import {RestHttpErrors} from '../coercion/rest-http-error'; +import {RestHttpErrors} from '..'; +import {AnyObject} from '@loopback/repository'; const toJsonSchema = require('openapi-schema-to-json-schema'); - const debug = debugModule('loopback:rest:validation'); +/** + * Check whether the request body is valid according to the provided OpenAPI schema. + * The JSON schema is generated from the OpenAPI schema which is typically defined + * by `@requestBody()`. + * The validation leverages AJS shema validator. + * @param body The body data from an HTTP request. + * @param requestBodySpec The OpenAPI requestBody specification defined in `@requestBody()`. + * @param globalSchemas The referenced schemas generated from `OpenAPISpec.components.schemas`. + */ export function validateRequestBody( // tslint:disable-next-line:no-any body: any, @@ -28,47 +36,33 @@ export function validateRequestBody( if (requestBodySpec && requestBodySpec.required && body == undefined) throw new HttpErrors.BadRequest('Request body is required'); - const schema = getRequestBodySchema(requestBodySpec, globalSchemas || {}); + const schema = getRequestBodySchema(requestBodySpec); debug('Request body schema: %j', util.inspect(schema, {depth: null})); if (!schema) return; const jsonSchema = convertToJsonSchema(schema); - validateValueAgainstJsonSchema(body, jsonSchema); + validateValueAgainstJsonSchema(body, jsonSchema, globalSchemas); } +/** + * Get the schema from requestBody specification. + * @param requestBodySpec The requestBody specification defined in `@requestBody()`. + */ function getRequestBodySchema( requestBodySpec: RequestBodyObject | undefined, - globalSchemas: SchemasObject, ): SchemaObject | undefined { if (!requestBodySpec) return; const content = requestBodySpec.content; // FIXME(bajtos) we need to find the entry matching the content-type // header from the incoming request (e.g. "application/json"). - const schema = content[Object.keys(content)[0]].schema; - if (!schema || !isReferenceObject(schema)) { - return schema; - } - - return resolveSchemaReference(schema.$ref, globalSchemas); -} - -function resolveSchemaReference(ref: string, schemas: SchemasObject) { - // A temporary solution for resolving schema references produced - // by @loopback/repository-json-schema. In the future, we should - // support arbitrary references anywhere in the OpenAPI spec. - // See https://github.com/strongloop/loopback-next/issues/435 - const match = ref.match(/^#\/components\/schemas\/([^\/]+)$/); - if (!match) throw new Error(`Unsupported schema reference format: ${ref}`); - const schemaId = match[1]; - - debug(`Resolving schema reference ${ref} (schema id ${schemaId}).`); - if (!(schemaId in schemas)) { - throw new Error(`Invalid reference ${ref} - schema ${schemaId} not found.`); - } - return schemas[schemaId]; + return content[Object.keys(content)[0]].schema; } +/** + * Convert an OpenAPI schema to the corresponding JSON schema. + * @param openapiSchema The OpenAPI schema to convert. + */ function convertToJsonSchema(openapiSchema: SchemaObject) { const jsonSchema = toJsonSchema(openapiSchema); delete jsonSchema['$schema']; @@ -79,11 +73,28 @@ function convertToJsonSchema(openapiSchema: SchemaObject) { return jsonSchema; } -// tslint:disable-next-line:no-any -function validateValueAgainstJsonSchema(body: any, schema: any) { - const ajv = new AJV({allErrors: true}); +/** + * Validate the request body data against JSON schema. + * @param body The request body data. + * @param schema The JSON schema used to perform the validation. + * @param globalSchemas Schema references. + */ +function validateValueAgainstJsonSchema( + // tslint:disable-next-line:no-any + body: any, + jsonSchema: AnyObject, + globalSchemas?: SchemasObject, +) { + const schemaWithRef = Object.assign({}, jsonSchema); + schemaWithRef.components = { + schemas: globalSchemas, + }; + + const ajv = new AJV({ + allErrors: true, + }); try { - if (ajv.validate(schema, body)) { + if (ajv.validate(schemaWithRef, body)) { debug('Request body passed AJV validation.'); return; } diff --git a/packages/rest/test/unit/request-body.validator.test.ts b/packages/rest/test/unit/request-body.validator.test.ts index a164e6e30727..6c7c417b9eca 100644 --- a/packages/rest/test/unit/request-body.validator.test.ts +++ b/packages/rest/test/unit/request-body.validator.test.ts @@ -6,7 +6,11 @@ import {expect} from '@loopback/testlab'; import {validateRequestBody} from '../../src/validation/request-body.validator'; import {aBodySpec} from '../helpers'; -import {RequestBodyObject, SchemasObject} from '@loopback/openapi-v3-types'; +import { + RequestBodyObject, + SchemaObject, + SchemasObject, +} from '@loopback/openapi-v3-types'; const TODO_SCHEMA = { title: 'Todo', @@ -18,6 +22,24 @@ const TODO_SCHEMA = { required: ['title'], }; +// a schema that contains a property with referenced schema +const ACCOUNT_SCHEMA = { + title: 'Account', + properties: { + title: {type: 'string'}, + address: {$ref: '#/components/schemas/Address'}, + }, +}; + +const ADDRESS_SCHEMA = { + title: 'Address', + properties: { + city: {type: 'string'}, + unit: {type: 'number'}, + isOwner: {type: 'boolean'}, + }, +}; + describe('validateRequestBody', () => { it('accepts valid data omitting optional properties', () => { validateRequestBody({title: 'work'}, aBodySpec(TODO_SCHEMA)); @@ -66,7 +88,7 @@ describe('validateRequestBody', () => { it('rejects empty values when body is required', () => { verifyValidationRejectsInputWithError( - /body is required/i, + /body is required/, null, aBodySpec(TODO_SCHEMA, {required: true}), ); @@ -77,7 +99,7 @@ describe('validateRequestBody', () => { }); it('rejects invalid values for number properties', () => { - const schema: SchemasObject = { + const schema: SchemaObject = { properties: { count: {type: 'number'}, }, @@ -88,6 +110,91 @@ describe('validateRequestBody', () => { aBodySpec(schema), ); }); + + context('rejects array of data with wrong type - ', () => { + it('primitive types', () => { + const schema: SchemaObject = { + type: 'object', + properties: { + orders: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }; + verifyValidationRejectsInputWithError( + /orders\[1\] should be string/, + {orders: ['order1', 1]}, + aBodySpec(schema), + ); + }); + + it('first level $ref', () => { + const schema: SchemaObject = { + type: 'array', + items: { + $ref: '#/components/schemas/Todo', + }, + }; + verifyValidationRejectsInputWithError( + /required property/, + [{title: 'a good todo'}, {description: 'a todo item missing title'}], + aBodySpec(schema), + {Todo: TODO_SCHEMA}, + ); + }); + + it('nested $ref in schema', () => { + const schema: SchemaObject = { + type: 'object', + properties: { + todos: { + type: 'array', + items: { + $ref: '#/components/schemas/Todo', + }, + }, + }, + }; + verifyValidationRejectsInputWithError( + /todos\[1\] should have required property \'title\'/, + { + todos: [ + {title: 'a good todo'}, + {description: 'a todo item missing title'}, + ], + }, + aBodySpec(schema), + {Todo: TODO_SCHEMA}, + ); + }); + + it('nested $ref in reference', () => { + const schema: SchemaObject = { + type: 'object', + properties: { + accounts: { + type: 'array', + items: { + $ref: '#/components/schemas/Account', + }, + }, + }, + }; + verifyValidationRejectsInputWithError( + /accounts\[0\]\.address\.city should be string/, + { + accounts: [ + {title: 'an account with invalid address', address: {city: 1}}, + ], + }, + aBodySpec(schema), + {Account: ACCOUNT_SCHEMA, Address: ADDRESS_SCHEMA}, + ); + }); + }); }); // ----- HELPERS ----- /