openapi-middleware
Version:
OpenAPI middleware for common api frameworks (pre-release version!)
871 lines (826 loc) • 24 kB
JavaScript
import { strict as assert } from 'node:assert';
import ParameterValidator from '../../lib/ParameterValidator.js';
import ParameterError from '../../lib/errors/ParameterError.js';
describe('unit: ParameterValidator', () => {
describe('check: required or optional', () => {
it('should throw when required param is not set', async () => {
const parameter = new ParameterValidator([
{
name: 'pathString',
in: 'path',
required: true,
description: 'user name.',
schema: { type: 'string' },
},
{
name: 'qsString',
in: 'query',
required: true,
description: 'user name.',
schema: { type: 'string' },
},
]);
assert.throws(() => parameter.test({}, {}), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'optional',
message: 'is missing and not optional',
property: '@.path.pathString',
},
{
code: null,
reason: 'optional',
message: 'is missing and not optional',
property: '@.qs.qsString',
},
]));
});
it('should not throw when optional param is not set', async () => {
const parameter = new ParameterValidator([
{
name: 'pathString',
in: 'path',
required: false,
description: 'user name.',
schema: { type: 'string' },
},
{
name: 'qsString',
in: 'query',
required: false,
description: 'user name.',
schema: { type: 'string' },
},
]);
parameter.test({}, {});
});
});
describe('check: data types', () => {
describe('type: primitives', () => {
let parameter;
beforeEach(() => {
const queryParams = [
{
name: 'qsString',
in: 'query',
required: true,
description: 'user name.',
schema: { type: 'string' },
},
{
name: 'qsNumber',
in: 'query',
required: true,
description: 'user name.',
schema: { type: 'number' },
},
{
name: 'qsInteger',
in: 'query',
required: true,
description: 'user name.',
schema: { type: 'integer' },
},
{
name: 'qsBoolean',
in: 'query',
required: true,
description: 'user name.',
schema: { type: 'boolean' },
},
];
const pathParams = queryParams.map((param) => ({
...param,
name: param.name.replace('qs', 'path'),
in: param.in.replace('query', 'path'),
}));
parameter = new ParameterValidator([...pathParams, ...queryParams]);
});
it('should throw when not correct data types', () => {
let expectedErrors = [
{
code: null,
reason: 'type',
message: 'must be string, but is number',
property: '@.path.pathString',
},
{
code: null,
reason: 'type',
message: 'must be number, but is string',
property: '@.path.pathNumber',
},
{
code: null,
reason: 'type',
message: 'must be integer, but is number',
property: '@.path.pathInteger',
},
{
code: null,
reason: 'type',
message: 'must be boolean, but is number',
property: '@.path.pathBoolean',
},
];
expectedErrors = expectedErrors.concat(expectedErrors.map((error) => ({
...error,
property: error.property.replace(/path/g, 'qs'),
})));
assert.throws(() => parameter.test({
pathString: 123,
pathNumber: '123',
pathInteger: 123.123,
pathBoolean: 123,
}, {
qsString: 123,
qsNumber: '123',
qsInteger: 123.123,
qsBoolean: 123,
}), new ParameterError('INPUT_VALIDATION_FAILED', expectedErrors));
});
it('should not throw when correct data types', () => {
parameter.test({
pathString: '123',
pathNumber: 123,
pathInteger: 123,
pathBoolean: true,
}, {
qsString: '123',
qsNumber: 123,
qsInteger: 123,
qsBoolean: true,
});
});
});
describe('type: object', () => {
let parameter;
beforeEach(() => {
parameter = new ParameterValidator([
{
name: 'pathObject',
in: 'path',
required: true,
description: 'user name.',
schema: {
type: 'object',
required: [
'firstName',
],
properties: {
firstName: {
type: 'string',
},
lastName: {
type: 'string',
},
},
},
},
{
name: 'qsObject',
in: 'query',
required: true,
description: 'user name.',
schema: {
type: 'object',
required: [
'firstName',
],
properties: {
firstName: {
type: 'string',
},
lastName: {
type: 'string',
},
},
},
},
]);
});
it('should throw when not object', () => {
assert.throws(() => parameter.test({ pathObject: '123' }, { qsObject: '123' }), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'type',
message: 'must be object, but is string',
property: '@.path.pathObject',
},
{
code: null,
reason: 'type',
message: 'must be object, but is string',
property: '@.qs.qsObject',
},
]));
});
it('should throw when object but missing required property', () => {
assert.throws(() => parameter.test({ pathObject: {} }, { qsObject: {} }), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'optional',
message: 'is missing and not optional',
property: '@.path.pathObject.firstName',
},
{
code: null,
reason: 'optional',
message: 'is missing and not optional',
property: '@.qs.qsObject.firstName',
},
]));
});
it('should throw when object but inner property is not a string', () => {
assert.throws(() => parameter.test({ pathObject: { firstName: 123 } }, { qsObject: { firstName: 123 } }), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'type',
message: 'must be string, but is number',
property: '@.path.pathObject.firstName',
},
{
code: null,
reason: 'type',
message: 'must be string, but is number',
property: '@.qs.qsObject.firstName',
},
]));
});
it('should not throw on optional property', () => {
parameter.test({ pathObject: { firstName: 'asd' } }, { qsObject: { firstName: 'asd' } });
});
});
describe('type: array', () => {
let parameter;
beforeEach(() => {
parameter = new ParameterValidator([
{
name: 'pathArray',
in: 'path',
required: true,
description: 'user name.',
schema: {
type: 'array',
items: {
type: 'string',
},
},
},
{
name: 'qsArray',
in: 'query',
required: true,
description: 'user name.',
schema: {
type: 'array',
items: {
type: 'string',
},
},
},
]);
});
it('should throw when not array', () => {
assert.throws(() => parameter.test({ pathArray: 'daniel' }, { qsArray: 'daniel' }), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'type',
message: 'must be array, but is string',
property: '@.path.pathArray',
},
{
code: null,
reason: 'type',
message: 'must be array, but is string',
property: '@.qs.qsArray',
},
]));
});
it('should throw when array of number', () => {
assert.throws(() => parameter.test({ pathArray: ['first name', 1] }, { qsArray: ['first name', 1] }), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'type',
message: 'must be string, but is number',
property: '@.path.pathArray[1]',
},
{
code: null,
reason: 'type',
message: 'must be string, but is number',
property: '@.qs.qsArray[1]',
},
]));
});
it('should not throw when array of strings', () => {
parameter.test({ pathArray: ['first name', 'last name'] }, { qsArray: ['first name', 'last name'] });
});
});
});
describe('check: number validators', () => {
describe('minimum / maximum', () => {
let parameter;
beforeEach(() => {
parameter = new ParameterValidator([
{
name: 'pathNumber',
in: 'path',
required: true,
schema: { type: 'number', minimum: 2, maximum: 4 },
},
{
name: 'qsNumber',
in: 'query',
required: true,
schema: { type: 'number', minimum: 2, maximum: 4 },
},
]);
});
// TODO: following should work with the validation lib
it.skip('should throw when not meeting number minimum', () => {
assert.throws(() => parameter.test({}, {
nameNumber: 1,
}), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'type',
message: 'must be number, but is string',
property: '@.qs.nameNumber',
},
]));
});
it.skip('should throw when not meeting number maximum', () => {
assert.throws(() => parameter.test({}, {
nameNumber: 8,
}), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'type',
message: 'must be number, but is string',
property: '@.qs.nameNumber',
},
]));
});
it('should not throw on ranged number', () => {
parameter.test({
pathNumber: 2,
}, {
qsNumber: 2,
});
});
});
describe('format', () => {
let parameter;
beforeEach(() => {
parameter = new ParameterValidator([
{
name: 'pathFloat',
in: 'path',
required: true,
schema: { type: 'number', format: 'float' },
},
{
name: 'pathDouble',
in: 'path',
required: true,
schema: { type: 'number', format: 'double' },
},
{
name: 'pathInt32',
in: 'path',
required: true,
schema: { type: 'number', format: 'int32' },
},
{
name: 'pathInt64',
in: 'path',
required: true,
schema: { type: 'number', format: 'int64' },
},
{
name: 'qsFloat',
in: 'query',
required: true,
schema: { type: 'number', format: 'float' },
},
{
name: 'qsDouble',
in: 'query',
required: true,
schema: { type: 'number', format: 'double' },
},
{
name: 'qsInt32',
in: 'query',
required: true,
schema: { type: 'number', format: 'int32' },
},
{
name: 'qsInt64',
in: 'query',
required: true,
schema: { type: 'number', format: 'int64' },
},
]);
});
it('should throw on format mismatch', () => {
assert.throws(() => parameter.test({
pathFloat: 123,
pathDouble: 123,
pathInt32: 123.123,
pathInt64: 123.123,
}, {
qsFloat: 123,
qsDouble: 123,
qsInt32: 123.123,
qsInt64: 123.123,
}), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'type',
message: 'must be integer, but is number',
property: '@.path.pathInt32',
},
{
code: null,
reason: 'type',
message: 'must be integer, but is number',
property: '@.path.pathInt64',
},
{
code: null,
reason: 'type',
message: 'must be integer, but is number',
property: '@.qs.qsInt32',
},
{
code: null,
reason: 'type',
message: 'must be integer, but is number',
property: '@.qs.qsInt64',
},
]));
});
it('should not throw when integer validation passed', () => {
parameter.test({
pathDouble: 123,
pathFloat: 123,
pathInt32: 123,
pathInt64: 123,
}, {
qsFloat: 123,
qsDouble: 123,
qsInt32: 123,
qsInt64: 123,
});
});
});
});
describe('check: string validators', () => {
describe('minLength / maxLength', () => {
let parameter;
beforeEach(() => {
parameter = new ParameterValidator([
{
name: 'pathString',
in: 'path',
required: true,
schema: { type: 'string', minLength: 2, maxLength: 4 },
},
{
name: 'qsString',
in: 'query',
required: true,
schema: { type: 'string', minLength: 2, maxLength: 4 },
},
]);
});
it('should fail on minLength', () => {
assert.throws(() => parameter.test({
pathString: 't',
}, {
qsString: 't',
}), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'minLength',
message: 'must be longer than 2 elements, but it has 1',
property: '@.path.pathString',
},
{
code: null,
reason: 'minLength',
message: 'must be longer than 2 elements, but it has 1',
property: '@.qs.qsString',
},
]));
});
it('should fail on maxLength', () => {
assert.throws(() => parameter.test({
pathString: 'test string',
}, {
qsString: 'test string',
}), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'maxLength',
message: 'must be shorter than 4 elements, but it has 11',
property: '@.path.pathString',
},
{
code: null,
reason: 'maxLength',
message: 'must be shorter than 4 elements, but it has 11',
property: '@.qs.qsString',
},
]));
});
it('should not throw', () => {
parameter.test({
pathString: 'test',
}, {
qsString: 'test',
});
});
});
describe('format', () => {
let parameter;
beforeEach(() => {
parameter = new ParameterValidator([
{
name: 'pathDate',
in: 'path',
required: true,
schema: { type: 'string', format: 'date' },
},
{
name: 'pathDateTime',
in: 'path',
required: true,
schema: { type: 'string', format: 'date-time' },
},
{
name: 'qsDate',
in: 'query',
required: true,
schema: { type: 'string', format: 'date' },
},
{
name: 'qsDateTime',
in: 'query',
required: true,
schema: { type: 'string', format: 'date-time' },
},
// {
// name: 'byte',
// in: 'query',
// required: true,
// schema: { type: 'string', format: 'byte', },
// },
// {
// name: 'binary',
// in: 'query',
// required: true,
// schema: { type: 'string', format: 'binary', },
// },
]);
});
it('should throw on format mismatch', () => {
assert.throws(() => parameter.test({
pathDate: '123',
pathDateTime: '123',
pathByte: '123',
pathBinary: '123',
}, {
qsDate: '123',
qsDateTime: '123',
qsByte: '123',
qsBinary: '123',
}), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'pattern',
message: 'must match [date], but is equal to "123"',
property: '@.path.pathDate',
},
{
code: null,
reason: 'pattern',
message: 'must match [date-time], but is equal to "123"',
property: '@.path.pathDateTime',
},
{
code: null,
reason: 'pattern',
message: 'must match [date], but is equal to "123"',
property: '@.qs.qsDate',
},
{
code: null,
reason: 'pattern',
message: 'must match [date-time], but is equal to "123"',
property: '@.qs.qsDateTime',
},
]));
});
});
describe('pattern', () => {
let parameter;
beforeEach(() => {
parameter = new ParameterValidator([
{
name: 'pathPattern',
in: 'path',
required: true,
schema: { type: 'string', pattern: /[0-9]/ },
},
{
name: 'qsPattern',
in: 'query',
required: true,
schema: { type: 'string', pattern: /[0-9]/ },
},
]);
});
it('should throw on mismatched pattern', () => {
assert.throws(() => parameter.test({
pathPattern: 'asdasd',
}, {
qsPattern: 'asdasd',
}), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'pattern',
message: 'must match [/[0-9]/], but is equal to "asdasd"',
property: '@.path.pathPattern',
},
{
code: null,
reason: 'pattern',
message: 'must match [/[0-9]/], but is equal to "asdasd"',
property: '@.qs.qsPattern',
},
]));
});
it('should not throw', () => {
parameter.test({
pathPattern: '123123',
}, {
qsPattern: '123123',
});
});
});
});
describe('check: nullable', () => {
let parameter;
beforeEach(() => {
parameter = new ParameterValidator([
{
name: 'pathNullable',
in: 'path',
required: true,
schema: { type: 'string', nullable: true },
},
{
name: 'pathNotNullable',
in: 'path',
required: true,
schema: { type: 'string', nullable: false },
},
{
name: 'qsNullable',
in: 'query',
required: true,
schema: { type: 'string', nullable: true },
},
{
name: 'qsNotNullable',
in: 'query',
required: true,
schema: { type: 'string', nullable: false },
},
]);
});
it('should not throw on null', () => {
assert.throws(() => parameter.test({
pathNullable: null,
pathNotNullable: null,
}, {
qsNullable: null,
qsNotNullable: null,
}), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'type',
message: 'must be string, but is null',
property: '@.path.pathNotNullable',
},
{
code: null,
reason: 'type',
message: 'must be string, but is null',
property: '@.qs.qsNotNullable',
},
]));
});
});
describe('check: requestBody', () => {
let parameter;
beforeEach(() => {
parameter = new ParameterValidator([], {
'application/json': {
type: 'object',
required: ['nameString'],
properties: {
nameString: {
description: 'user name.',
type: 'string',
},
nameObject: {
type: 'object',
required: ['firstName'],
properties: {
firstName: {
type: 'string',
},
lastName: {
type: 'string',
},
profile: {
type: 'object',
required: ['image'],
properties: {
image: {
type: 'string',
},
},
},
},
},
},
},
});
});
it('should throw on nameString that got number', () => {
assert.throws(() => parameter.test({}, {}, {
'application/json': {
nameString: 123,
},
}), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'type',
message: 'must be string, but is number',
property: '@.requestBody["application/json"].nameString',
},
]));
});
it('should throw on nameObject missing required property', () => {
assert.throws(() => parameter.test({}, {}, {
'application/json': {
nameString: '123',
nameObject: {
profile: {},
},
},
}), new ParameterError('INPUT_VALIDATION_FAILED', [
{
code: null,
reason: 'optional',
message: 'is missing and not optional',
property: '@.requestBody["application/json"].nameObject.firstName',
},
{
code: null,
reason: 'optional',
message: 'is missing and not optional',
property: '@.requestBody["application/json"].nameObject.profile.image',
},
]));
});
it('should not throw on nameObject when profile is missing', () => {
parameter.test({}, {}, {
'application/json': {
nameString: '123',
nameObject: {
firstName: 'test',
},
},
});
});
it('should not throw on nameObject when profile is missing', () => {
parameter.test({}, {}, {
'application/json': {
nameString: '123',
nameObject: {
firstName: 'test',
profile: {
image: 'test',
},
},
},
});
});
});
});