serverless/test/unit/lib/classes/config-schema-handler/normalize-ajv-errors.test.js
2024-05-29 11:51:04 -04:00

433 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'))
})
})