UNPKG

validata

Version:

Type safe data validation and sanitization

389 lines (344 loc) 12.2 kB
import { jsonDateParser } from 'json-date-parser'; import { isDate } from './date'; import { asNumber, isNumber } from './number'; import { asObject, isObject, maybeAsObject, maybeObject } from './object'; import { asString, isString, maybeString } from './string'; import { expectIssue, expectSuccess, expectValue, runTests } from './test-helpers'; import { isIssue, Issue } from './types'; interface MyObject { a: number; b: string; } interface ParentObject { o: MyObject; s: string; } describe('isObject', () => { it('will fail non-object', () => { const fut = isObject(); expectIssue(fut, null, 'not-defined'); expectIssue(fut, undefined, 'not-defined'); expectIssue(fut, 0, 'incorrect-type'); expectIssue(fut, new Date(), 'incorrect-type'); expectIssue(fut, [], 'incorrect-type'); expectIssue(fut, 'test', 'incorrect-type'); }); it('will accept object', () => { const fut = isObject(); expectSuccess(fut, {}); expectSuccess(fut, { a: 47 }); }); it('will process children', () => { const fut = isObject<MyObject>({ a: asNumber({ coerceMin: 25 }), b: asString(), }); expectValue(fut, { a: 47, b: 'asd' }, { a: 47, b: 'asd' }); expectValue(fut, { a: '47', b: 12 }, { a: 47, b: '12' }); expectValue(fut, { a: '7', b: 12 }, { a: 25, b: '12' }); }); it('will process nested children', () => { const fut = isObject<ParentObject>({ o: isObject<MyObject>({ a: isNumber(), b: asString(), }), s: asString(), }); runTests(fut, { input: { o: 47, s: 'asd' }, issues: [{ reason: 'incorrect-type', path: ['o'] }], }, { input: { o: {}, s: 'asd' }, issues: [{ reason: 'not-defined', path: ['o', 'a'] }], }, { input: { o: { a: 'hello', b: 'hello' }, s: 'asd' }, issues: [{ reason: 'incorrect-type', path: ['o', 'a'] }], }, { expect: { o: { a: 12, b: '12' }, s: 'asd' }, input: { o: { a: 12, b: 12 }, s: 'asd' }, }, ); }); it('will process children', () => { const fut = isObject<MyObject>({ a: isNumber({ min: 25 }), b: isString(), }); expectValue(fut, { a: 47, b: 'asd' }, { a: 47, b: 'asd' }); expectIssue(fut, { a: '47', b: 'asd' }, 'incorrect-type', ['a']); expectIssue(fut, { a: 47, b: 'asd', c: 234 }, 'unexpected-property', ['c']); expectIssue(fut, {}, 'not-defined', ['a']); expectIssue(fut, {}, 'not-defined', ['b']); }); it('will handle optional property', () => { const fut = isObject<{ a: number, b?: string }>({ a: isNumber(), b: maybeString(), }); expectIssue(fut, {}, 'not-defined', ['a']); expectIssue(fut, { b: 'asd' }, 'not-defined', ['a']); { const result = expectSuccess(fut, { a: 42 }); expect(result.value).not.toHaveProperty('b'); } { const result = expectSuccess(fut, { a: 42, b: undefined }); expect(result.value).toHaveProperty('b'); expect(result.value.b).toEqual(undefined); } { const result = expectSuccess(fut, { a: 42, b: null }); expect(result.value).toHaveProperty('b'); expect(result.value.b).toEqual(undefined); } }); it('will error on unexpected properties', () => { const fut = isObject<MyObject>({ a: isNumber({ min: 25 }), b: isString(), }); expectIssue(fut, { a: 47, b: 'asd', c: 234 }, 'unexpected-property', ['c']); }); it('will strip unexpected properties', () => { const fut = isObject<MyObject>({ a: isNumber({ min: 25 }), b: isString(), }, { stripExtraProperties: true }); expectValue(fut, { a: 47, b: 'asd', c: 345, d: 'hello' }, { a: 47, b: 'asd' }); }); it('will ignore unexpected properties', () => { const fut = isObject<MyObject>({ a: isNumber({ min: 25 }), b: isString(), }, { ignoreExtraProperties: true }); expectValue(fut, { a: 47, b: 'asd', c: 345, d: 'hello' }, { a: 47, b: 'asd', c: 345, d: 'hello' } as MyObject); }); it('will check with custom validator returning custom issues', () => { const fut = isObject<MyObject>({ a: isNumber({ min: 25 }), b: isString({ validator: (value, _options, path) => value === 'triggerCustom' ? [Issue.forPath(path ?? [], value, 'custom')] : true }), }); expectIssue(fut, { a: 47, b: 'triggerCustom' }, 'custom', ['b']); }); }); describe('maybeObject', () => { it('will coerce null and undefined', () => { const fut = maybeObject(); expectValue(fut, null, undefined); expectValue(fut, undefined, undefined); }); it('will fail non-object', () => { const fut = maybeObject(); expectIssue(fut, 0, 'incorrect-type'); expectIssue(fut, new Date(), 'incorrect-type'); expectIssue(fut, [], 'incorrect-type'); expectIssue(fut, 'test', 'incorrect-type'); }); it('will accept non-object if requested', () => { const fut = maybeObject({}, { incorrectTypeToUndefined: true }); expectValue(fut, 0, undefined); expectValue(fut, new Date(), undefined); expectValue(fut, [], undefined); expectValue(fut, 'test', undefined); }); it('will accept object', () => { const fut = maybeObject(); expectSuccess(fut, {}); expectSuccess(fut, { a: 47 }); }); }); describe('asObject', () => { it('will fail non-object', () => { const fut = asObject(); expectIssue(fut, null, 'not-defined'); expectIssue(fut, undefined, 'not-defined'); expectIssue(fut, 0, 'no-conversion'); expectIssue(fut, new Date(), 'no-conversion'); expectIssue(fut, [], 'no-conversion'); expectIssue(fut, 'test', 'no-conversion'); }); it('will use default', () => { const fut = asObject<MyObject>({ a: asNumber(), b: asString(), }, { default: { a: 47, b: 'default' } }); expectValue(fut, null, { a: 47, b: 'default' }); expectValue(fut, undefined, { a: 47, b: 'default' }); }); it('will accept object', () => { const fut = asObject(); expectSuccess(fut, {}); expectSuccess(fut, { a: 47 }); }); it('will process children', () => { const fut = asObject<MyObject>({ a: asNumber({ coerceMin: 25 }), b: asString(), }); expectValue(fut, { a: 47, b: 'asd' }, { a: 47, b: 'asd' }); expectValue(fut, { a: '47', b: 12 }, { a: 47, b: '12' }); expectValue(fut, { a: '7', b: 12 }, { a: 25, b: '12' }); }); it('will process nested children', () => { const fut = asObject<ParentObject>({ o: isObject<MyObject>({ a: isNumber(), b: asString(), }), s: asString(), }); runTests(fut, { input: { o: 47, s: 'asd' }, issues: [{ reason: 'incorrect-type', path: ['o'] }], }, { input: { o: {}, s: 'asd' }, issues: [{ reason: 'not-defined', path: ['o', 'a'] }], }, { input: { o: { a: 'hello', b: 'hello' }, s: 'asd' }, issues: [{ reason: 'incorrect-type', path: ['o', 'a'] }], }, { expect: { o: { a: 12, b: '12' }, s: 'asd' }, input: { o: { a: 12, b: 12 }, s: 'asd' }, }, ); }); it('will process children', () => { const fut = asObject<MyObject>({ a: isNumber({ min: 25 }), b: isString(), }); expectValue(fut, { a: 47, b: 'asd' }, { a: 47, b: 'asd' }); expectIssue(fut, { a: '47', b: 'asd' }, 'incorrect-type', ['a']); expectIssue(fut, { a: 47, b: 'asd', c: 234 }, 'unexpected-property', ['c']); expectIssue(fut, {}, 'not-defined', ['a']); expectIssue(fut, {}, 'not-defined', ['b']); }); it('will error on unexpected properties', () => { const fut = asObject<MyObject>({ a: isNumber({ min: 25 }), b: isString(), }); expectIssue(fut, { a: 47, b: 'asd', c: 234 }, 'unexpected-property', ['c']); }); it('will strip unexpected properties', () => { const fut = asObject<MyObject>({ a: isNumber({ min: 25 }), b: isString(), }, { stripExtraProperties: true }); expectValue(fut, { a: 47, b: 'asd', c: 345, d: 'hello' }, { a: 47, b: 'asd' }); }); describe('string parsing', () => { const fut = asObject<MyObject>({ a: isNumber({ min: 25 }), b: isString(), }); it('will fail empty string', () => { expectIssue(fut, '', 'no-conversion'); }); it('will fail nonJSON string', () => { expectIssue(fut, 'testing', 'no-conversion'); }); it('will fail invalid JSON string', () => { expectIssue(fut, '{testing=12}', 'no-conversion'); expectIssue(fut, '{testing:12}', 'no-conversion'); }); it('will parse valid JSON string that matches requirements', () => { expectValue(fut, '{"a": 123, "b": "test"}', { a: 123, b: 'test' }); }); it('will parse valid JSON string that doesn\'t match requirements', () => { expectIssue(fut, '{}', 'not-defined', ['a']); expectIssue(fut, '{}', 'not-defined', ['b']); expectIssue(fut, '{"a": "123", "b": "test"}', 'incorrect-type', ['a']); }); }); }); describe('maybeAsObject', () => { it('will coerce null and undefined', () => { const fut = maybeAsObject(); expectValue(fut, null, undefined); expectValue(fut, undefined, undefined); }); it('will use default', () => { const fut = maybeAsObject<MyObject>({ a: asNumber(), b: asString(), }, { default: { a: 47, b: 'default' } }); expectValue(fut, null, { a: 47, b: 'default' }); expectValue(fut, undefined, { a: 47, b: 'default' }); }); it('will convert non-object to undefined', () => { const fut = maybeAsObject(); expectValue(fut, 0, undefined); expectValue(fut, new Date(), undefined); expectValue(fut, [], undefined); expectValue(fut, 'test', undefined); }); it('will fail invalid JSON string when parsing is strict', () => { const fut = maybeAsObject({}, { strictParsing: true }); expectIssue(fut, 0, 'no-conversion'); expectIssue(fut, 'test', 'no-conversion'); expectIssue(fut, '{testing=12}', 'no-conversion'); expectIssue(fut, '{testing:12}', 'no-conversion'); }); it('will accept non-object if requested', () => { const fut = maybeAsObject({}, { incorrectTypeToUndefined: true }); expectValue(fut, 0, undefined); expectValue(fut, new Date(), undefined); expectValue(fut, [], undefined); expectValue(fut, 'test', undefined); }); it('will accept object', () => { const fut = maybeAsObject(); expectSuccess(fut, {}); expectSuccess(fut, { a: 47 }); }); describe('string parsing', () => { const fut = maybeAsObject<MyObject>({ a: isNumber({ min: 25 }), b: isString(), }); it('will convert empty string to undefined', () => { expectValue(fut, '', undefined); }); it('will convert nonJSON string to undefined', () => { expectValue(fut, 'testing', undefined); }); it('will convert invalid JSON string to undefined', () => { expectValue(fut, '{testing=12}', undefined); expectValue(fut, '{testing:12}', undefined); }); it('will use custom converter', () => { type TestDateType = { d: Date }; const fut = asObject<TestDateType>( { d: isDate() }, { converter: (value) => typeof value !== 'string' ? undefined : JSON.parse(value, jsonDateParser) as TestDateType } ); expectIssue(fut, '{"d":12}', 'incorrect-type', ['d']); const date = '2021-01-13T20:23:36.164Z'; const r = fut.process(`{"d":"${date}"}`); expect(r).toBeDefined(); if (isIssue(r)) { fail(`Unexpected issue: ${JSON.stringify(r)}`); } expect(r.value.d.constructor.name).toEqual('Date'); expect(r.value.d.getTime()).toEqual(new Date(date).getTime()); }); it('will parse valid JSON string that matches requirements', () => { expectValue(fut, '{"a": 123, "b": "test"}', { a: 123, b: 'test' }); }); it('will parse valid JSON string that doesn\'t match requirements', () => { expectIssue(fut, '{}', 'not-defined', ['a']); expectIssue(fut, '{}', 'not-defined', ['b']); expectIssue(fut, '{"a": "123", "b": "test"}', 'incorrect-type', ['a']); }); }); });