UNPKG

@kravc/schema

Version:

Advanced JSON schema manipulation and validation library.

356 lines (284 loc) 11.1 kB
'use strict' const { load } = require('js-yaml') const { expect } = require('chai') const { readFileSync } = require('fs') const { Schema, Validator } = require('src') const loadSync = (yamlPath) => { const id = yamlPath.split('.')[0].split('/').reverse()[0] const source = load(readFileSync(yamlPath)) return new Schema(source, id) } const SCHEMAS = [ 'examples/Status.yaml', 'examples/Profile.yaml', 'examples/Preferences.yaml', 'examples/FavoriteItem.yaml' ].map(path => loadSync(path)) describe('Validator', () => { describe('Validator.constructor(schemas)', () => { it('create validator for schemas', () => { new Validator(SCHEMAS) }) it('throws error if no schemas provided', () => { expect( () => new Validator() ).to.throw('No schemas provided') }) it('throws error if referenced schema not found', () => { const entitySchema = new Schema({ name: { $ref: 'MissingSchema' } }, 'Entity') expect( () => new Validator([ ...SCHEMAS, entitySchema ]) ).to.throw('Schemas validation failed:') }) }) describe('.validate(object, schemaId, shouldNullifyEmptyValues = false, shouldCleanupNulls = true)', () => { it('returns validated, cleaned and normalized object', () => { const validator = new Validator(SCHEMAS) const _createdAt = new Date().toISOString() const input = { name: 'Oleksandr', toBeRemoved: null, contactDetails: { email: 'a@kra.vc', toBeRemoved: null, }, favoriteItems: [ { id: '1', name: 'Student Book', categories: [ 'Education' ], toBeRemoved: null, _createdAt }, ], locations: [{ name: 'Home', address: { type: 'Primary', zip: '03119', city: 'Kyiv', addressLine1: 'Melnikova 83-D, 78', _createdAt }, _createdAt }], preferences: { height: 180, isNotificationEnabled: true, _createdAt }, status: 'Active', _createdAt } const validInput = validator.validate(input, 'Profile', false, true) expect(validInput.toBeRemoved).to.not.exist expect(validInput.contactDetails.toBeRemoved).to.not.exist expect(validInput.favoriteItems[0].toBeRemoved).to.not.exist expect(validInput._createdAt).to.not.exist expect(validInput.preferences._createdAt).to.not.exist expect(validInput.locations[0]._createdAt).to.not.exist expect(validInput.locations[0].address._createdAt).to.not.exist expect(validInput.favoriteItems[0]._createdAt).to.not.exist expect(validInput.name).to.eql('Oleksandr') expect(validInput.gender).to.eql('Other') expect(validInput.status).to.eql('Active') expect(validInput.locations[0].name).to.eql('Home') expect(validInput.locations[0].address.country).to.eql('Ukraine') expect(validInput.locations[0].address.zip).to.eql('03119') expect(validInput.locations[0].address.city).to.eql('Kyiv') expect(validInput.locations[0].address.addressLine1).to.eql('Melnikova 83-D, 78',) expect(validInput.locations[0].address.type).to.eql('Primary') expect(validInput.favoriteItems[0].id).to.eql('1') expect(validInput.favoriteItems[0].name).to.eql('Student Book') expect(validInput.favoriteItems[0].categories).to.deep.eql([ 'Education' ]) expect(validInput.favoriteItems[0].status).to.eql('Pending') expect(validInput.contactDetails.email).to.eql('a@kra.vc') expect(validInput.contactDetails.mobileNumber).to.eql('380504112171') expect(validInput.preferences.height).to.eql(180) expect(validInput.preferences.isNotificationEnabled).to.eql(true) }) it('normalizes object attributes according to property type', () => { const validator = new Validator(SCHEMAS) const input = { name: 'Oleksandr', contactDetails: { email: 'a@kra.vc' }, preferences: { height: '180', isNotificationEnabled: 'true' } } let validInput validInput = validator.validate(input, 'Profile') expect(validInput.preferences.height).to.eql(180) expect(validInput.preferences.isNotificationEnabled).to.eql(true) input.preferences.isNotificationEnabled = '1' validInput = validator.validate(input, 'Profile') expect(validInput.preferences.isNotificationEnabled).to.eql(true) input.preferences.isNotificationEnabled = '0' validInput = validator.validate(input, 'Profile') expect(validInput.preferences.isNotificationEnabled).to.eql(false) input.preferences.isNotificationEnabled = 0 validInput = validator.validate(input, 'Profile') expect(validInput.preferences.isNotificationEnabled).to.eql(false) expect(() => { input.preferences.isNotificationEnabled = 'NaN' validInput = validator.validate(input, 'Profile') expect(validInput.preferences.isNotificationEnabled).to.eql('NaN') }).to.throw('"Profile" validation failed') expect(() => { input.preferences.isNotificationEnabled = 0 input.preferences.height = 'NaN' validInput = validator.validate(input, 'Profile') expect(validInput.preferences.height).to.eql('NaN') }).to.throw('"Profile" validation failed') }) it('throws validation error if cleanup or normalize method failed', () => { const validator = new Validator(SCHEMAS) const input = { name: 'Oleksandr', contactDetails: { email: 'a@kra.vc' }, favoriteItems: 'NOT_ARRAY_BUT_STRING' } try { validator.validate(input, 'Profile') } catch (validationError) { const error = validationError.toJSON() expect(error.object).to.exist expect(error.code).to.eql('ValidationError') expect(error.message).to.eql('"Profile" validation failed') expect(error.schemaId).to.eql('Profile') const errorMessage = error.validationErrors[0].message expect(errorMessage).to.eql('Expected type array but found type string') return } throw new Error('Validation error is not thrown') }) it('throws error if validation failed', () => { const validator = new Validator(SCHEMAS) const input = {} try { validator.validate(input, 'Profile') } catch (validationError) { const error = validationError.toJSON() expect(error.object).to.exist expect(error.code).to.eql('ValidationError') expect(error.message).to.eql('"Profile" validation failed') expect(error.schemaId).to.eql('Profile') expect(error.validationErrors).to.have.lengthOf(2) return } throw new Error('Validation error is not thrown') }) it('throws error if schema not found', () => { const validator = new Validator(SCHEMAS) expect( () => validator.validate({}, 'Account') ).to.throw('Schema "Account" not found') }) it('throws error if multiple schemas with same id', async () => { const exampleSchema1 = new Schema({ number: { required: true } }, 'Example') const exampleSchema2 = new Schema({ id: {} }, 'Example') expect(() => new Validator([ exampleSchema1, exampleSchema2 ])) .to.throw('Multiple "Example" schemas provided') }) }) describe('.validate(object, schemaId, shouldNullifyEmptyValues = false)', () => { it('throws validation error for attributes not matching format or pattern', () => { const validator = new Validator(SCHEMAS) const input = { name: 'Oleksandr', gender: '', contactDetails: { email: 'a@kra.vc', secondaryEmail: '', mobileNumber: '', }, } expect( () => validator.validate(input, 'Profile') ).to.throw('"Profile" validation failed') }) }) describe('.validate(object, schemaId, shouldNullifyEmptyValues = true)', () => { it('returns input with cleaned up null values for not required attributes', () => { const validator = new Validator(SCHEMAS) const input = { name: 'Oleksandr', gender: '', // ENUM contactDetails: { email: 'a@kra.vc', mobileNumber: '', // PATTERN secondaryEmail: '', // FORMAT }, } const validInput = validator.validate(input, 'Profile', true) expect(validInput.gender).to.eql(null) expect(validInput.contactDetails.mobileNumber).to.eql(null) expect(validInput.contactDetails.secondaryEmail).to.eql(null) }) it('throws validation errors for other attributes', () => { const validator = new Validator(SCHEMAS) const input = { name: '', // code: MIN_LENGTH gender: 'NONE', // code: ENUM_MISMATCH contactDetails: { email: 'a@kra.vc', mobileNumber: 'abc', // code: PATTERN secondaryEmail: '', }, preferences: { age: 'a' // code: INVALID_TYPE }, } try { validator.validate(input, 'Profile', true) } catch (validationError) { const error = validationError.toJSON() expect(error.message).to.eql('"Profile" validation failed') expect(error.validationErrors).to.have.lengthOf(4) expect(error.validationErrors[0].code).to.eql('INVALID_TYPE') expect(error.validationErrors[1].code).to.eql('PATTERN') expect(error.validationErrors[2].code).to.eql('ENUM_MISMATCH') expect(error.validationErrors[3].code).to.eql('MIN_LENGTH') return } throw new Error('Validation error is not thrown') }) }) describe('.normalize(object, schemaId)', () => { it('returns normalized object clone', () => { const validator = new Validator(SCHEMAS) const input = {} const normalizedInput = validator.normalize(input, 'Profile') expect(normalizedInput.gender).to.eql('Other') expect(normalizedInput.status).to.eql('Pending') }) it('throws error if schema not found', () => { const validator = new Validator(SCHEMAS) expect( () => validator.normalize({}, 'Account') ).to.throw('Schema "Account" not found') }) }) describe('.schemasMap', () => { it('returns schemas map', () => { const validator = new Validator(SCHEMAS) expect(validator.schemasMap).to.exist }) }) describe('.getReferenceIds(schemaId)', () => { it('returns ids of referenced schemas', () => { const validator = new Validator(SCHEMAS) const referenceIds = validator.getReferenceIds('Profile') expect(referenceIds).to.eql([ 'Status', 'FavoriteItem', 'Preferences' ]) }) }) })