Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

add async validation support #4762

Merged
merged 1 commit into from Mar 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
73 changes: 38 additions & 35 deletions packages/rest/src/__tests__/unit/request-body.validator.test.ts
Expand Up @@ -51,17 +51,17 @@ const INVALID_ACCOUNT_SCHEMA = {
};

describe('validateRequestBody', () => {
it('accepts valid data omitting optional properties', () => {
validateRequestBody(
it('accepts valid data omitting optional properties', async () => {
await validateRequestBody(
{value: {title: 'work'}, schema: TODO_SCHEMA},
aBodySpec(TODO_SCHEMA),
);
});

// Test for https://github.com/strongloop/loopback-next/issues/3234
it('honors options for AJV validator caching', () => {
it('honors options for AJV validator caching', async () => {
// 1. Trigger a validation with `{coerceTypes: false}`
validateRequestBody(
await validateRequestBody(
{
value: {city: 'San Jose', unit: 123, isOwner: true},
schema: ADDRESS_SCHEMA,
Expand All @@ -72,7 +72,7 @@ describe('validateRequestBody', () => {
);

// 2. Trigger a validation with `{coerceTypes: true}`
validateRequestBody(
await validateRequestBody(
{
value: {city: 'San Jose', unit: '123', isOwner: 'true'},
schema: ADDRESS_SCHEMA,
Expand All @@ -83,7 +83,7 @@ describe('validateRequestBody', () => {
);

// 3. Trigger a validation with `{coerceTypes: false}` with invalid data
expect(() =>
await expect(
validateRequestBody(
{
value: {city: 'San Jose', unit: '123', isOwner: true},
Expand All @@ -93,10 +93,10 @@ describe('validateRequestBody', () => {
{},
{coerceTypes: false},
),
).to.throw(/The request body is invalid/);
).to.be.rejectedWith(/The request body is invalid/);
});

it('rejects data missing a required property', () => {
it('rejects data missing a required property', async () => {
const details: RestHttpErrors.ValidationErrorDetails[] = [
{
path: '',
Expand All @@ -105,7 +105,7 @@ describe('validateRequestBody', () => {
info: {missingProperty: 'title'},
},
];
verifyValidationRejectsInputWithError(
await verifyValidationRejectsInputWithError(
INVALID_MSG,
'VALIDATION_FAILED',
details,
Expand All @@ -116,7 +116,7 @@ describe('validateRequestBody', () => {
);
});

it('rejects data containing values of a wrong type', () => {
it('rejects data containing values of a wrong type', async () => {
const details: RestHttpErrors.ValidationErrorDetails[] = [
{
path: '/isComplete',
Expand All @@ -125,7 +125,7 @@ describe('validateRequestBody', () => {
info: {type: 'boolean'},
},
];
verifyValidationRejectsInputWithError(
await verifyValidationRejectsInputWithError(
INVALID_MSG,
'VALIDATION_FAILED',
details,
Expand All @@ -137,7 +137,7 @@ describe('validateRequestBody', () => {
);
});

it('reports all validation errors', () => {
it('reports all validation errors', async () => {
const details: RestHttpErrors.ValidationErrorDetails[] = [
{
path: '',
Expand All @@ -152,7 +152,7 @@ describe('validateRequestBody', () => {
info: {type: 'boolean'},
},
];
verifyValidationRejectsInputWithError(
await verifyValidationRejectsInputWithError(
INVALID_MSG,
'VALIDATION_FAILED',
details,
Expand All @@ -164,15 +164,18 @@ describe('validateRequestBody', () => {
);
});

it('reports schema generation errors', () => {
expect(() =>
validateRequestBody({value: {}, schema: INVALID_ACCOUNT_SCHEMA}),
).to.throw(
it('reports schema generation errors', async () => {
await expect(
validateRequestBody({
value: {},
schema: INVALID_ACCOUNT_SCHEMA,
}),
).to.be.rejectedWith(
"can't resolve reference #/components/schemas/Invalid from id #",
);
});

it('resolves schema references', () => {
it('resolves schema references', async () => {
const details: RestHttpErrors.ValidationErrorDetails[] = [
{
path: '',
Expand All @@ -181,7 +184,7 @@ describe('validateRequestBody', () => {
info: {missingProperty: 'title'},
},
];
verifyValidationRejectsInputWithError(
await verifyValidationRejectsInputWithError(
INVALID_MSG,
'VALIDATION_FAILED',
details,
Expand All @@ -191,8 +194,8 @@ describe('validateRequestBody', () => {
);
});

it('rejects empty values when body is required', () => {
verifyValidationRejectsInputWithError(
it('rejects empty values when body is required', async () => {
await verifyValidationRejectsInputWithError(
'Request body is required',
'MISSING_REQUIRED_PARAMETER',
undefined,
Expand All @@ -203,14 +206,14 @@ describe('validateRequestBody', () => {
);
});

it('allows empty values when body is optional', () => {
validateRequestBody(
it('allows empty values when body is optional', async () => {
await validateRequestBody(
{value: null, schema: TODO_SCHEMA},
aBodySpec(TODO_SCHEMA, {required: false}),
);
});

it('rejects invalid values for number properties', () => {
it('rejects invalid values for number properties', async () => {
const details: RestHttpErrors.ValidationErrorDetails[] = [
{
path: '/count',
Expand All @@ -224,7 +227,7 @@ describe('validateRequestBody', () => {
count: {type: 'number'},
},
};
verifyValidationRejectsInputWithError(
await verifyValidationRejectsInputWithError(
INVALID_MSG,
'VALIDATION_FAILED',
details,
Expand All @@ -234,7 +237,7 @@ describe('validateRequestBody', () => {
});

context('rejects array of data with wrong type - ', () => {
it('primitive types', () => {
it('primitive types', async () => {
const details: RestHttpErrors.ValidationErrorDetails[] = [
{
path: '/orders/1',
Expand All @@ -254,7 +257,7 @@ describe('validateRequestBody', () => {
},
},
};
verifyValidationRejectsInputWithError(
await verifyValidationRejectsInputWithError(
INVALID_MSG,
'VALIDATION_FAILED',
details,
Expand All @@ -263,7 +266,7 @@ describe('validateRequestBody', () => {
);
});

it('first level $ref', () => {
it('first level $ref', async () => {
const details: RestHttpErrors.ValidationErrorDetails[] = [
{
path: '/1',
Expand All @@ -278,7 +281,7 @@ describe('validateRequestBody', () => {
$ref: '#/components/schemas/Todo',
},
};
verifyValidationRejectsInputWithError(
await verifyValidationRejectsInputWithError(
INVALID_MSG,
'VALIDATION_FAILED',
details,
Expand All @@ -288,7 +291,7 @@ describe('validateRequestBody', () => {
);
});

it('nested $ref in schema', () => {
it('nested $ref in schema', async () => {
const details: RestHttpErrors.ValidationErrorDetails[] = [
{
path: '/todos/1',
Expand All @@ -314,7 +317,7 @@ describe('validateRequestBody', () => {
},
},
};
verifyValidationRejectsInputWithError(
await verifyValidationRejectsInputWithError(
INVALID_MSG,
'VALIDATION_FAILED',
details,
Expand All @@ -330,7 +333,7 @@ describe('validateRequestBody', () => {
);
});

it('nested $ref in reference', () => {
it('nested $ref in reference', async () => {
const details: RestHttpErrors.ValidationErrorDetails[] = [
{
path: '/accounts/0/address/city',
Expand All @@ -350,7 +353,7 @@ describe('validateRequestBody', () => {
},
},
};
verifyValidationRejectsInputWithError(
await verifyValidationRejectsInputWithError(
INVALID_MSG,
'VALIDATION_FAILED',
details,
Expand All @@ -368,7 +371,7 @@ describe('validateRequestBody', () => {

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

function verifyValidationRejectsInputWithError(
async function verifyValidationRejectsInputWithError(
expectedMessage: string,
expectedCode: string,
expectedDetails: RestHttpErrors.ValidationErrorDetails[] | undefined,
Expand All @@ -378,7 +381,7 @@ function verifyValidationRejectsInputWithError(
required?: boolean,
) {
try {
validateRequestBody(
await validateRequestBody(
{value: body, schema},
aBodySpec(schema, {required}),
schemas,
Expand Down
11 changes: 8 additions & 3 deletions packages/rest/src/parser.ts
Expand Up @@ -54,14 +54,14 @@ export async function parseOperationArgs(
);
}

function buildOperationArguments(
async function buildOperationArguments(
operationSpec: OperationObject,
request: Request,
pathParams: PathParameterValues,
body: RequestBody,
globalSchemas: SchemasObject,
options: RequestBodyValidationOptions = {},
): OperationArgs {
): Promise<OperationArgs> {
let requestBodyIndex = -1;
if (operationSpec.requestBody) {
// the type of `operationSpec.requestBody` could be `RequestBodyObject`
Expand All @@ -88,7 +88,12 @@ function buildOperationArguments(
}

debug('Validating request body - value %j', body);
validateRequestBody(body, operationSpec.requestBody, globalSchemas, options);
await validateRequestBody(
body,
operationSpec.requestBody,
globalSchemas,
options,
);

if (requestBodyIndex > -1) paramArgs.splice(requestBodyIndex, 0, body.value);
return paramArgs;
Expand Down
24 changes: 16 additions & 8 deletions packages/rest/src/validation/request-body.validator.ts
Expand Up @@ -33,7 +33,7 @@ const ajvErrors = require('ajv-errors');
* @param globalSchemas - The referenced schemas generated from `OpenAPISpec.components.schemas`.
* @param options - Request body validation options for AJV
*/
export function validateRequestBody(
export async function validateRequestBody(
achrinza marked this conversation as resolved.
Show resolved Hide resolved
body: RequestBody,
requestBodySpec?: RequestBodyObject,
globalSchemas: SchemasObject = {},
Expand Down Expand Up @@ -68,7 +68,7 @@ export function validateRequestBody(
if (!schema) return;

options = Object.assign({coerceTypes: !!body.coercionRequired}, options);
validateValueAgainstSchema(body.value, schema, globalSchemas, options);
await validateValueAgainstSchema(body.value, schema, globalSchemas, options);
}

/**
Expand Down Expand Up @@ -117,7 +117,7 @@ function getKeyForOptions(options: RequestBodyValidationOptions) {
* @param globalSchemas - Schema references.
* @param options - Request body validation options.
*/
function validateValueAgainstSchema(
async function validateValueAgainstSchema(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
body: any,
schema: SchemaObject | ReferenceObject,
Expand All @@ -142,13 +142,18 @@ function validateValueAgainstSchema(
cache.set(schema, validatorMap);
}

if (validate(body)) {
debug('Request body passed AJV validation.');
return;
let validationErrors: AJV.ErrorObject[] = [];
try {
const validationResult = await validate(body);
// When body is optional & values is empty / null, ajv returns null
if (validationResult || validationResult === null) {
debug('Request body passed AJV validation.');
return;
}
} catch (error) {
validationErrors = error.errors;
}

let validationErrors = validate.errors as AJV.ErrorObject[];

/* istanbul ignore if */
if (debug.enabled) {
debug(
Expand Down Expand Up @@ -212,5 +217,8 @@ function createValidator(
ajvErrors(ajv, options.ajvErrors);
}

// See https://ajv.js.org/#asynchronous-validation for async validation
schemaWithRef.$async = true;

return ajv.compile(schemaWithRef);
}