serverless/test/unit/lib/classes/config-schema-handler/normalize-ajv-errors.test.js

422 lines
13 KiB
JavaScript

'use strict';
const { expect } = require('chai');
const Ajv = require('ajv').default;
const memoize = require('memoizee');
const normalizeAjvErrors = require('../../../../../lib/classes/config-schema-handler/normalize-ajv-errors');
describe('#normalizeAjvErrors', () => {
const resolveAjv = memoize(
() => new Ajv({ allErrors: true, coerceTypes: 'array', verbose: true })
);
const resolveValidate = memoize((schema) => resolveAjv().compile(schema));
const schema = {
type: 'object',
properties: {
provider: {
anyOf: [
{
type: 'object',
properties: {
name: { const: 'aws' },
deploymentBucket: {
type: 'object',
properties: { maxPreviousDeploymentArtifacts: { type: 'number' } },
},
},
},
{
type: 'object',
properties: {
name: { const: 'other' },
otherProp: {
type: 'object',
properties: { foo: { type: 'number' } },
},
},
},
],
},
custom: {
type: 'object',
properties: {
someCustom: {
anyOf: [
{
type: 'object',
properties: {
name: { const: 'first' },
},
},
{
type: 'object',
properties: {
name: { const: 'second' },
},
},
],
},
someString: {
anyOf: [
{
type: 'string',
pattern: 'foo',
},
{
type: 'string',
pattern: 'bar',
},
{ type: 'object' },
],
},
},
},
package: {
type: 'object',
properties: {
include: { type: 'array', items: { type: 'string' } },
},
additionalProperties: false,
},
functions: {
type: 'object',
patternProperties: {
'^[a-zA-Z0-9-_]+$': {
type: 'object',
properties: {
handler: {
type: 'string',
},
image: {
type: 'object',
properties: {
workingDirectory: {
type: 'string',
},
command: {
type: 'array',
items: {
type: 'string',
},
},
entryPoint: {
type: 'array',
items: {
type: 'string',
},
},
},
dependencies: {
command: ['entryPoint'],
entryPoint: ['command'],
workingDirectory: ['entryPoint', 'command'],
},
additionalProperties: false,
},
events: {
type: 'array',
items: {
anyOf: [
{
type: 'object',
properties: { __schemaWorkaround__: { const: null } },
required: ['__schemaWorkaround__'],
additionalProperties: false,
},
{
type: 'object',
properties: {
http: {
anyOf: [
{ type: 'string', pattern: '^(get|post|put) [a-zA-Z0-9]+$' },
{
type: 'object',
properties: {
path: { type: 'string' },
method: { type: 'string' },
},
required: ['path', 'method'],
additionalProperties: false,
},
],
},
},
required: ['http'],
additionalProperties: false,
},
],
},
},
},
required: ['handler'],
additionalProperties: false,
},
'additionalProperties': false,
},
additionalProperties: false,
},
},
additionalProperties: false,
};
const resolveNormalizedErrors = (config) => {
const validate = resolveValidate(schema);
validate(config);
if (!validate.errors) return [];
return normalizeAjvErrors(validate.errors);
};
let errors;
before(() => {
errors = resolveNormalizedErrors({
foo: 'bar',
provider: {
name: 'aws',
deploymentBucket: { maxPreviousDeploymentArtifacts: 'foo' },
},
custom: {
someCustom: { name: 'third' },
someString: 'other',
},
package: { incclude: ['./folder'] },
functions: {
'invalid name': {},
'foo': {
handler: 'foo',
image: {
workingDirectory: 'bar',
},
events: [
{
bar: {},
},
{
http: { path: '/foo', method: 'GET' },
method: 'GET',
},
{
http: null,
method: 'GET',
},
{
http: { path: '/foo', method: 'GET', other: 'foo' },
},
{
http: 'gets foo',
},
{
http: { method: 'GET' },
},
],
},
},
});
});
describe('Reporting', () => {
it('should report error for unrecognized root property', () =>
expect(
errors.some((error) => {
if (error.instancePath !== '') return false;
if (error.keyword !== 'additionalProperties') return false;
error.isExpected = true;
return true;
})
).to.be.true);
it('should report error for unrecognized deep level property', () =>
expect(
errors.some((error) => {
if (error.instancePath !== '/package') return false;
if (error.keyword !== 'additionalProperties') return false;
error.isExpected = true;
return true;
})
).to.be.true);
it('should report error for invalid function name', () =>
expect(
errors.some((error) => {
if (error.instancePath !== '/functions') return false;
if (error.keyword !== 'additionalProperties') return false;
error.isExpected = true;
return true;
})
).to.be.true);
it('should report error for unrecognized event', () =>
expect(
errors.some((error) => {
if (error.instancePath !== '/functions/foo/events/0') return false;
if (error.keyword !== 'anyOf') return false;
error.isExpected = true;
return true;
})
).to.be.true);
it('should report error for unrecognized property at event type configuration level', () =>
expect(
errors.some((error) => {
if (error.instancePath !== '/functions/foo/events/1') return false;
if (error.keyword !== 'additionalProperties') return false;
error.isExpected = true;
return true;
})
).to.be.true);
it(
'should report error for unrecognized property at event type configuration level, ' +
'as result of improper indentation in YAML config',
() =>
// Catches following yaml issue:
//
// functions:
// foo:
// events:
// - http:
// method: GET # Should be additionally indented
expect(
errors.some((error) => {
if (error.instancePath !== '/functions/foo/events/2') return false;
if (error.keyword !== 'additionalProperties') return false;
error.isExpected = true;
return true;
})
).to.be.true
);
it(
'should report error in anyOf case, where two types are possible (string and object), ' +
'and object with unrecognized property was used',
() =>
expect(
errors.some((error) => {
if (error.instancePath !== '/functions/foo/events/3/http') return false;
if (error.keyword !== 'additionalProperties') return false;
error.isExpected = true;
return true;
})
).to.be.true
);
it(
'should report error in anyOf case, where two types are possible (string and object), ' +
'and invalid string was used',
() =>
expect(
errors.some((error) => {
if (error.instancePath !== '/functions/foo/events/4/http') return false;
if (error.keyword !== 'pattern') return false;
error.isExpected = true;
return true;
})
).to.be.true
);
it(
'should report in anyOf case, where two types are possible (string and object), ' +
'and object with missing required property was used',
() =>
expect(
errors.some((error) => {
if (error.instancePath !== '/functions/foo/events/5/http') return false;
if (error.keyword !== 'required') return false;
error.isExpected = true;
return true;
})
).to.be.true
);
it(
'should report in anyOf case, where two values of same (object) type are possible ' +
'and for one variant error for deeper path was reported',
() =>
expect(
errors.some((error) => {
if (
error.instancePath !== '/provider/deploymentBucket/maxPreviousDeploymentArtifacts'
) {
return false;
}
if (error.keyword !== 'type') return false;
error.isExpected = true;
return true;
})
).to.be.true
);
it(
'should report in anyOf case, where two values of same (object) type are possible ' +
'and for all variants errors relate to paths of same depth',
() =>
expect(
errors.some((error) => {
if (error.instancePath !== '/custom/someCustom') {
return false;
}
if (error.keyword !== 'anyOf') return false;
error.isExpected = true;
return true;
})
).to.be.true
);
it(
'should report in anyOf case, where two values of same (string) type are possible ' +
'and for all variants errors relate to paths of same depth',
() =>
expect(
errors.some((error) => {
if (error.instancePath !== '/custom/someString') {
return false;
}
if (error.keyword !== 'anyOf') return false;
error.isExpected = true;
return true;
})
).to.be.true
);
it('should report the duplicated erorr message if more than one dependency is missing only once', () => {
const depsErrors = errors.filter((item) => item.keyword === 'dependencies');
expect(depsErrors).to.have.lengthOf(1);
depsErrors[0].isExpected = true;
});
it('should not report side errors', () =>
expect(errors.filter((error) => !error.isExpected)).to.deep.equal([]));
});
describe('Message customization', () => {
it('should report "additionalProperties" error with meaningful message', () =>
expect(
errors.find((error) => {
if (error.instancePath !== '/package') return false;
if (error.keyword !== 'additionalProperties') return false;
return true;
}).message
).to.include('unrecognized property '));
it('should report invalid function name error with meaningful message', () =>
expect(
errors.find((error) => {
if (error.instancePath !== '/functions') return false;
if (error.keyword !== 'additionalProperties') return false;
return true;
}).message
).to.include('must be alphanumeric'));
it('should report unrecognized event error with a meaningful message', () =>
expect(
errors.find((error) => {
if (error.instancePath !== '/functions/foo/events/0') return false;
if (error.keyword !== 'anyOf') return false;
return true;
}).message
).to.include('unsupported function event'));
it('should report value which do not match multiple constants with a meaningful message', () =>
expect(
errors.find((error) => {
if (error.instancePath !== '/custom/someCustom') return false;
if (error.keyword !== 'anyOf') return false;
return true;
}).message
).to.include('unsupported value'));
it('should report value which do not match multiple string formats with a meaningful message', () =>
expect(
errors.find((error) => {
if (error.instancePath !== '/custom/someString') return false;
if (error.keyword !== 'anyOf') return false;
return true;
}).message
).to.include('unsupported string format'));
});
});