UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

685 lines (641 loc) 13.6 kB
import { deepCoerce, getDate, getDateNow, mapValuesDeep, validateEntity, } from '.'; import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import { mapDateTimeFormatToEitherStringOrObject } from '../models/schema'; describe('utils', () => { describe('#getDate', () => { it('returns current Date', () => { expect(getDate().getTime()).toBeLessThanOrEqual(Date.now()); }); it('returns exact Date', () => { expect(getDate('2021-01-01T00:00:00.000Z').getTime()).toEqual( new Date('2021-01-01T00:00:00.000Z').getTime(), ); }); }); describe('#getDateNow', () => { it('returns current time', () => { expect(getDateNow()).toBeLessThanOrEqual(Date.now()); }); }); describe('#mapValuesDeep', () => { it('returns the current object without handler', () => { expect( mapValuesDeep({ a: 1, }), ).toEqual({ a: 1, }); }); it('returns the mapped value', () => { expect(mapValuesDeep({ a: 1 }, (v) => v + 1)).toEqual({ a: 2, }); }); it('returns the nested mapped value', () => { expect( mapValuesDeep({ a: { b: 1 } }, (v) => typeof v === 'number' ? v + 1 : v, ), ).toEqual({ a: { b: 2 }, }); }); it('returns the nested mapped value in arrays', () => { expect( mapValuesDeep({ a: [1] }, (v) => (typeof v === 'number' ? v + 1 : v)), ).toEqual({ a: [2], }); }); }); describe('#deepCoerce', () => { it('does nothing on schemas not needing update', () => { expect( deepCoerce( { value: 1, }, { properties: { value: { type: 'number', }, }, }, ), ).toEqual({ value: 1, }); }); it('does nothing on string values', () => { expect( deepCoerce( { value: '1', }, { properties: { value: { type: 'string', }, }, }, ), ).toEqual({ value: '1', }); }); it('does nothing on array items', () => { expect( deepCoerce( { value: ['1', 1], }, { properties: { value: { type: 'array', }, }, }, ), ).toEqual({ value: ['1', 1], }); }); it('coerces number values', () => { expect( deepCoerce( { value: '1', }, { properties: { value: { type: 'number', }, }, }, ), ).toEqual({ value: 1, }); }); it('does noop on not valid number value', () => { expect( deepCoerce( { value: { $exists: true, }, }, { properties: { value: { type: 'number', }, }, }, ), ).toEqual({ value: { $exists: true, }, }); }); it('coerces number values supporting arrays', () => { expect( deepCoerce( { value: ['1'], }, { properties: { value: { type: 'number', }, }, }, ), ).toEqual({ value: [1], }); }); it('coerces integer values', () => { expect( deepCoerce( { value: '1', }, { properties: { value: { type: 'integer', }, }, }, ), ).toEqual({ value: 1, }); }); it('does noop on not valid integer value', () => { expect( deepCoerce( { value: { $exists: true, }, }, { properties: { value: { type: 'integer', }, }, }, ), ).toEqual({ value: { $exists: true, }, }); }); it('coerces integer values supporting arrays', () => { expect( deepCoerce( { value: ['1'], }, { properties: { value: { type: 'integer', }, }, }, ), ).toEqual({ value: [1], }); }); it('coerces boolean values (1)', () => { expect( deepCoerce( { value: '1', }, { properties: { value: { type: 'boolean', }, }, }, ), ).toEqual({ value: true, }); }); it('coerces boolean values (true)', () => { expect( deepCoerce( { value: 'true', }, { properties: { value: { type: 'boolean', }, }, }, ), ).toEqual({ value: true, }); }); it('coerces boolean values supporting arrays', () => { expect( deepCoerce( { value: ['true', 'false'], }, { properties: { value: { type: 'boolean', }, }, }, undefined, [], true, ), ).toEqual({ value: [true, false], }); }); it('coerces array values', () => { expect( deepCoerce( { value: ['1', 1], }, { properties: { value: { type: 'array', items: { type: 'integer', }, }, }, }, ), ).toEqual({ value: [1, 1], }); }); it('does nothing on nested schemas not needing update', () => { expect( deepCoerce( { nested: { value: 1, }, }, { properties: { nested: { properties: { value: { type: 'number', }, }, }, }, }, ), ).toEqual({ nested: { value: 1, }, }); }); it('coerces format: date (string)', () => { expect( deepCoerce( { value: '2020-01-01T00:00:00.000Z', }, { properties: { value: { type: 'string', format: 'date', }, }, }, ), ).toEqual({ value: new Date('2020-01-01T00:00:00.000Z'), }); }); it('coerces format: date (number)', () => { expect( deepCoerce( { value: new Date('2020-01-01T00:00:00.000Z').getTime(), }, { properties: { value: { type: 'string', format: 'date', }, }, }, ), ).toEqual({ value: new Date('2020-01-01T00:00:00.000Z'), }); }); it('coerces format: date-time', () => { expect( deepCoerce( { value: '2020-01-01', }, { properties: { value: { type: 'string', format: 'date-time', }, }, }, ), ).toEqual({ value: new Date('2020-01-01'), }); }); it('coerces format: date on $exists property', () => { expect( deepCoerce( { value: { $exists: true, }, }, { properties: { value: { type: 'string', format: 'date', }, }, }, ), ).toEqual({ value: { $exists: true, }, }); }); it('coerces on sub keys not mapped on a schema (MongoDB like queries)', () => { expect( deepCoerce( { value: { $gt: '2020-01-01', }, value_2: { sub: { sub: { sub: '2020-01-01', }, }, }, }, { properties: { value: { type: 'string', format: 'date-time', }, value_2: { type: 'string', format: 'date-time', }, }, }, ), ).toEqual({ value: { $gt: new Date('2020-01-01'), }, value_2: { sub: { sub: { sub: new Date('2020-01-01'), }, }, }, }); }); it('coerces on nested schemas', () => { expect( deepCoerce( { nested: { value: '2020-01-01', }, }, { properties: { nested: { properties: { value: { type: 'string', format: 'date', }, }, }, }, }, ), ).toEqual({ nested: { value: new Date('2020-01-01'), }, }); }); it('performs a noop on frozen object', () => { expect( deepCoerce( Object.freeze({ nested: { value: '2020-01-01', }, }), { properties: { nested: { properties: { value: { type: 'string', format: 'date', }, }, }, }, }, ), ).toEqual({ nested: { value: '2020-01-01', }, }); }); }); describe('#validateEntity', () => { let validator; beforeEach(() => { validator = new Ajv({ strict: false, schemas: [], useDefaults: true, }); // @ts-ignore addFormats(validator); }); it('returns true if the entity is validating the schema', () => { expect( validateEntity( validator, { hello: 'john', }, { type: 'object', }, ), ).toEqual(true); }); it('uses cache schema instead of second validation', () => { const addSchemaSpy = jest.spyOn(validator, 'addSchema'); validateEntity( validator, { hello: 'john', }, { type: 'object', }, ); expect( validateEntity( validator, { hello: 'john', }, { type: 'object', }, ), ).toEqual(true); expect(addSchemaSpy).toHaveBeenCalledTimes(1); }); it('validates dates as strings', () => { expect( validateEntity( validator, { created_at: new Date(), }, mapDateTimeFormatToEitherStringOrObject({ type: 'object', properties: { created_at: { type: 'string', format: 'date-time', }, }, }), ), ).toEqual(true); }); it('validates dates in arrays', () => { expect( validateEntity( validator, { valid_dates: [new Date()], }, mapDateTimeFormatToEitherStringOrObject({ type: 'object', properties: { valid_dates: { type: 'array', items: { type: 'string', format: 'date-time', }, }, }, }), ), ).toEqual(true); }); it('throws an error on invalid entity', () => { let error; try { validateEntity( validator, { number: 'invalid', }, { type: 'object', properties: { number: { type: 'number', }, }, }, ); } catch (err) { error = err; } expect(error.message).toEqual('State schema validation error'); expect(error).toHaveProperty('entity', { number: 'invalid', }); expect(error).toHaveProperty('details', [ { instancePath: '/number', keyword: 'type', message: 'must be number', params: { type: 'number', }, schemaPath: '#/properties/number/type', }, { entity: { number: 'invalid', }, }, ]); }); }); });