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