Skip to content

Commit

Permalink
feat(rest): add query parameter validation
Browse files Browse the repository at this point in the history
  • Loading branch information
YaelGit committed Feb 13, 2019
1 parent 93f7bbf commit 2e12925
Show file tree
Hide file tree
Showing 16 changed files with 417 additions and 30 deletions.
3 changes: 2 additions & 1 deletion packages/rest/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"src/providers/reject.provider.ts",
"src/providers/send.provider.ts",
"src/router/routing-table.ts",
"src/validation/request-body.validator.ts"
"src/validation/request-body.validator.ts",
"src/validation/request-query.validator.ts"
],
"codeSectionDepth": 4
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
post,
Request,
requestBody,
RequestBody,
ValueWithSchema,
RestApplication,
} from '../../..';

Expand Down Expand Up @@ -97,10 +97,10 @@ class MultipartFormDataBodyParser implements BodyParser {
return mediaType.startsWith(FORM_DATA);
}

async parse(request: Request): Promise<RequestBody> {
async parse(request: Request): Promise<ValueWithSchema> {
const storage = multer.memoryStorage();
const upload = multer({storage});
return new Promise<RequestBody>((resolve, reject) => {
return new Promise<ValueWithSchema>((resolve, reject) => {
// tslint:disable-next-line:no-any
upload.any()(request, {} as any, err => {
if (err) reject(err);
Expand Down
4 changes: 2 additions & 2 deletions packages/rest/src/__tests__/unit/body-parser.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
JsonBodyParser,
RawBodyParser,
Request,
RequestBody,
ValueWithSchema,
RequestBodyParser,
RequestBodyParserOptions,
StreamBodyParser,
Expand Down Expand Up @@ -254,7 +254,7 @@ describe('body parser', () => {
describe('x-parser extension', () => {
let spec: OperationObject;
let req: Request;
let requestBody: RequestBody;
let requestBody: ValueWithSchema;

it('skips body parsing', async () => {
await loadRequestBodyWithXStream('stream');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('coerce object param - optional', function() {
test(OPTIONAL_ANY_OBJECT, {key: 'value'}, {key: 'value'});
test(OPTIONAL_ANY_OBJECT, undefined, undefined);
test(OPTIONAL_ANY_OBJECT, '', undefined);
test(OPTIONAL_ANY_OBJECT, 'null', null);
test(OPTIONAL_ANY_OBJECT, {key: 'null'}, {key: 'null'});
});

context('nested values are not coerced', () => {
Expand Down
193 changes: 193 additions & 0 deletions packages/rest/src/__tests__/unit/request.query.validator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import {expect} from '@loopback/testlab';
import {validateRequestQuery} from '../../validation/request-query.validator';
import {RestHttpErrors} from '../../';
import {aBodySpec} from '../helpers';
import {
ReferenceObject,
SchemaObject,
SchemasObject,
} from '@loopback/openapi-v3-types';

const INVALID_MSG = RestHttpErrors.INVALID_REQUEST_QUERY_MESSAGE;

const PING_SCHEMA = {
properties: {
pageSize: {type: 'integer', minimum: 0, maximum: 100, multipleOf: 5},
pageNumber: {type: 'number', minimum: 10, maximum: 200, multipleOf: 3},
pageBool: {type: 'boolean'},
pageName: {type: 'string', maxLength: 5, minLength: 1, pattern: '[abc]+'},
},
required: ['pageSize'],
};

describe('validateRequestQuery', () => {
it('accepts valid data omitting optional properties', () => {
validateRequestQuery(
{value: {pageSize: 5}, schema: PING_SCHEMA},
aBodySpec(PING_SCHEMA),
);
});

it('rejects data missing a required property', () => {
const details: RestHttpErrors.ValidationErrorDetails[] = [
{
path: '',
code: 'required',
message: "should have required property 'pageSize'",
info: {missingProperty: 'pageSize'},
},
];
verifyValidationRejectsInputWithError(
INVALID_MSG,
'VALIDATION_FAILED',
details,
{
description: 'missing required "pageSize"',
},
PING_SCHEMA,
);
});

it('rejects data containing values of a wrong type', () => {
const details: RestHttpErrors.ValidationErrorDetails[] = [
{
path: '.pageBool',
code: 'type',
message: 'should be boolean',
info: {type: 'boolean'},
},
];
verifyValidationRejectsInputWithError(
INVALID_MSG,
'VALIDATION_FAILED',
details,
{
pageSize: 5,
pageBool: 1111,
},
PING_SCHEMA,
);
});

it('rejects invalid values for number properties', () => {
const details: RestHttpErrors.ValidationErrorDetails[] = [
{
path: '.pageNumber',
code: 'type',
message: 'should be number',
info: {type: 'number'},
},
];
const schema: SchemaObject = {
properties: {
pageNumber: {type: 'number'},
},
};
verifyValidationRejectsInputWithError(
INVALID_MSG,
'VALIDATION_FAILED',
details,
{pageNumber: 'string value'},
schema,
);
});

it('rejects invalid values for number properties', () => {
const details: RestHttpErrors.ValidationErrorDetails[] = [
{
path: '.pageNumber',
code: 'type',
message: 'should be number',
info: {type: 'number'},
},
];
const schema: SchemaObject = {
properties: {
pageNumber: {type: 'number'},
},
};
verifyValidationRejectsInputWithError(
INVALID_MSG,
'VALIDATION_FAILED',
details,
{pageNumber: 'string value'},
schema,
);
});

it('rejects invalid values for number properties', () => {
const details: RestHttpErrors.ValidationErrorDetails[] = [
{
path: '.pageSize',
code: 'type',
message: 'should be number',
info: {type: 'number'},
},
{
path: '.pageNumber',
code: 'type',
message: 'should be number',
info: {type: 'number'},
},
{
path: '.pageBool',
code: 'type',
message: 'should be boolean',
info: {type: 'boolean'},
},
{
path: '.pageName',
code: 'type',
message: 'should be string',
info: {type: 'string'},
},
];
const schema: SchemaObject = {
properties: {
pageSize: {type: 'number'},
pageNumber: {type: 'number'},
pageBool: {type: 'boolean'},
pageName: {type: 'string'},
},
};
verifyValidationRejectsInputWithError(
INVALID_MSG,
'VALIDATION_FAILED',
details,
{
pageSize: 'string value',
pageNumber: 'string value',
pageBool: 1111,
pageName: 123,
},
schema,
);
});
});

// ----- HELPERS ----- /

function verifyValidationRejectsInputWithError(
expectedMessage: string,
expectedCode: string,
expectedDetails: RestHttpErrors.ValidationErrorDetails[] | undefined,
query: object | null,
schema: SchemaObject | ReferenceObject,
schemas?: SchemasObject,
required?: boolean,
) {
try {
validateRequestQuery(
{value: query, schema},
aBodySpec(schema, {required}),
schemas,
);
throw new Error(
"expected Function { name: 'validateRequestQuery' } to throw exception",
);
} catch (err) {
expect(err.message).to.equal(expectedMessage);
expect(err.code).to.equal(expectedCode);
expect(err.details).to.deepEqual(expectedDetails);
}
}
4 changes: 2 additions & 2 deletions packages/rest/src/body-parsers/body-parser.json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
invokeBodyParserMiddleware,
builtinParsers,
} from './body-parser.helpers';
import {BodyParser, RequestBody} from './types';
import {BodyParser, ValueWithSchema} from './types';
import {sanitizeJsonParse} from '../parse-json';

export class JsonBodyParser implements BodyParser {
Expand All @@ -34,7 +34,7 @@ export class JsonBodyParser implements BodyParser {
return !!is(mediaType, '*/json', '*/*+json');
}

async parse(request: Request): Promise<RequestBody> {
async parse(request: Request): Promise<ValueWithSchema> {
let body = await invokeBodyParserMiddleware(this.jsonParser, request);
// https://github.com/expressjs/body-parser/blob/master/lib/types/json.js#L71-L76
const contentLength = request.get('content-length');
Expand Down
4 changes: 2 additions & 2 deletions packages/rest/src/body-parsers/body-parser.raw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
invokeBodyParserMiddleware,
builtinParsers,
} from './body-parser.helpers';
import {BodyParser, RequestBody} from './types';
import {BodyParser, ValueWithSchema} from './types';

/**
* Parsing the request body into Buffer
Expand All @@ -35,7 +35,7 @@ export class RawBodyParser implements BodyParser {
return !!is(mediaType, 'application/octet-stream');
}

async parse(request: Request): Promise<RequestBody> {
async parse(request: Request): Promise<ValueWithSchema> {
const body = await invokeBodyParserMiddleware(this.rawParser, request);
return {value: body};
}
Expand Down
4 changes: 2 additions & 2 deletions packages/rest/src/body-parsers/body-parser.stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// License text available at https://opensource.org/licenses/MIT

import {Request} from '../types';
import {BodyParser, RequestBody} from './types';
import {BodyParser, ValueWithSchema} from './types';
import {builtinParsers} from './body-parser.helpers';

/**
Expand All @@ -21,7 +21,7 @@ export class StreamBodyParser implements BodyParser {
return false;
}

async parse(request: Request): Promise<RequestBody> {
async parse(request: Request): Promise<ValueWithSchema> {
return {value: request};
}
}
4 changes: 2 additions & 2 deletions packages/rest/src/body-parsers/body-parser.text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
invokeBodyParserMiddleware,
builtinParsers,
} from './body-parser.helpers';
import {BodyParser, RequestBody} from './types';
import {BodyParser, ValueWithSchema} from './types';

export class TextBodyParser implements BodyParser {
name = builtinParsers.text;
Expand All @@ -37,7 +37,7 @@ export class TextBodyParser implements BodyParser {
return !!is(mediaType, 'text/*');
}

async parse(request: Request): Promise<RequestBody> {
async parse(request: Request): Promise<ValueWithSchema> {
const body = await invokeBodyParserMiddleware(this.textParser, request);
return {value: body};
}
Expand Down
6 changes: 3 additions & 3 deletions packages/rest/src/body-parsers/body-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
import {
BodyParser,
BodyParserFunction,
RequestBody,
ValueWithSchema,
REQUEST_BODY_PARSER_TAG,
} from './types';

Expand All @@ -45,7 +45,7 @@ export class RequestBodyParser {
async loadRequestBodyIfNeeded(
operationSpec: OperationObject,
request: Request,
): Promise<RequestBody> {
): Promise<ValueWithSchema> {
const {requestBody, customParser} = await this._matchRequestBodySpec(
operationSpec,
request,
Expand Down Expand Up @@ -78,7 +78,7 @@ export class RequestBodyParser {
operationSpec: OperationObject,
request: Request,
) {
const requestBody: RequestBody = {
const requestBody: ValueWithSchema = {
value: undefined,
};
if (!operationSpec.requestBody) return {requestBody};
Expand Down
4 changes: 2 additions & 2 deletions packages/rest/src/body-parsers/body-parser.urlencoded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
invokeBodyParserMiddleware,
builtinParsers,
} from './body-parser.helpers';
import {BodyParser, RequestBody} from './types';
import {BodyParser, ValueWithSchema} from './types';

export class UrlEncodedBodyParser implements BodyParser {
name = builtinParsers.urlencoded;
Expand All @@ -32,7 +32,7 @@ export class UrlEncodedBodyParser implements BodyParser {
return !!is(mediaType, 'urlencoded');
}

async parse(request: Request): Promise<RequestBody> {
async parse(request: Request): Promise<ValueWithSchema> {
const body = await invokeBodyParserMiddleware(
this.urlencodedParser,
request,
Expand Down
6 changes: 3 additions & 3 deletions packages/rest/src/body-parsers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {Request} from '../types';
/**
* Request body with metadata
*/
export type RequestBody = {
export type ValueWithSchema = {
/**
* Parsed value of the request body
*/
Expand Down Expand Up @@ -46,13 +46,13 @@ export interface BodyParser {
* Parse the request body
* @param request http request
*/
parse(request: Request): Promise<RequestBody>;
parse(request: Request): Promise<ValueWithSchema>;
}

/**
* Plain function for body parsing
*/
export type BodyParserFunction = (request: Request) => Promise<RequestBody>;
export type BodyParserFunction = (request: Request) => Promise<ValueWithSchema>;

/**
* Binding tag for request body parser extensions
Expand Down
Loading

0 comments on commit 2e12925

Please sign in to comment.