UNPKG

n4s

Version:

typed schema validation version of enforce

206 lines (177 loc) 6.44 kB
import { describe, it, expect } from 'vitest'; import { enforce } from '../../../n4s'; describe('enforce.record()', () => { describe('lazy', () => { it('should return a rule instance', () => { const rule = enforce.record(enforce.isNumber()); expect(rule).toHaveProperty('run'); expect(rule).toHaveProperty('infer'); }); describe('value-only validation', () => { it('passes when all values match rule', () => { const rule = enforce.record(enforce.isNumber()); expect(rule.run({ a: 1, b: 2, c: 3 }).pass).toBe(true); }); it('fails when any value does not match', () => { const rule = enforce.record(enforce.isNumber()); // @ts-expect-error - testing runtime with invalid value type expect(rule.run({ a: 1, b: 'two', c: 3 }).pass).toBe(false); }); it('passes for empty objects', () => { const rule = enforce.record(enforce.isNumber()); expect(rule.run({}).pass).toBe(true); }); it('fails for non-object values', () => { const rule = enforce.record(enforce.isNumber()); // @ts-expect-error - testing runtime with non-object expect(rule.run('not an object').pass).toBe(false); }); it('fails for arrays', () => { const rule = enforce.record(enforce.isNumber()); // @ts-expect-error - testing runtime with array expect(rule.run([1, 2, 3]).pass).toBe(false); }); it('skips prototype keys', () => { const obj = Object.create({ inherited: 'bad' }); obj.own = 42; const rule = enforce.record(enforce.isNumber()); expect(rule.run(obj).pass).toBe(true); }); it('fails on prototype pollution keys', () => { const rule = enforce.record(enforce.isNumber()); const polluter = JSON.parse('{"__proto__": {"malicious": true}}'); const res = rule.run(polluter); expect(res.pass).toBe(false); expect(res.path).toEqual(['__proto__']); }); }); describe('key + value validation', () => { it('validates both keys and values', () => { const rule = enforce.record( enforce.isString().matches(/^[a-z]+$/), enforce.isNumber(), ); expect(rule.run({ abc: 1, def: 2 }).pass).toBe(true); }); it('fails when key does not match', () => { const rule = enforce.record( enforce.isString().matches(/^[a-z]+$/), enforce.isNumber(), ); const res = rule.run({ INVALID: 1 }); expect(res.pass).toBe(false); expect(res.path).toEqual(['INVALID']); }); it('fails when value does not match', () => { const rule = enforce.record( enforce.isString().matches(/^[a-z]+$/), enforce.isNumber(), ); // @ts-expect-error - testing runtime with invalid value type const res = rule.run({ valid: 'text' }); expect(res.pass).toBe(false); expect(res.path).toEqual(['valid']); }); }); describe('nested schemas', () => { it('validates nested shapes as values', () => { const rule = enforce.record( enforce.shape({ name: enforce.isString(), age: enforce.isNumber(), }), ); expect( rule.run({ user1: { name: 'Alice', age: 30 }, user2: { name: 'Bob', age: 25 }, }).pass, ).toBe(true); const failing = rule.run({ user1: { name: 'Alice', age: 30 }, // @ts-expect-error - testing runtime with invalid nested value user2: { name: 'Bob', age: '25' }, }); expect(failing.pass).toBe(false); expect(failing.path).toEqual(['user2', 'age']); }); it('validates nested records', () => { const rule = enforce.record(enforce.record(enforce.isBoolean())); expect( rule.run({ group1: { a: true, b: false }, group2: { c: true }, }).pass, ).toBe(true); }); }); describe('lazy chain type tests', () => { it('works with .test()', () => { const rule = enforce.record(enforce.isString()); expect(rule.test({ a: 'hello' })).toBe(true); }); it('chains with message() correctly', () => { const rule = enforce.record(enforce.isNumber().message('custom error')); const result = rule.run({ a: 1, b: 'err' }); expect(result.pass).toBe(false); expect(result.message).toEqual('custom error'); }); }); }); describe('eager', () => { describe('value-only validation', () => { it('passes when all values match rule', () => { expect(() => { enforce({ a: 1, b: 2, c: 3 }).record(enforce.isNumber()); }).not.toThrow(); }); it('fails when any value does not match', () => { expect(() => { enforce({ a: 1, b: 'two', c: 3 }).record(enforce.isNumber()); }).toThrow(); }); it('passes for empty objects', () => { expect(() => { enforce({}).record(enforce.isNumber()); }).not.toThrow(); }); it('fails for non-object values', () => { expect(() => { // @ts-expect-error - testing runtime with non-object enforce('not an object').record(enforce.isNumber()); }).toThrow(); }); it('fails for arrays', () => { expect(() => { // @ts-expect-error - testing runtime with array enforce([1, 2, 3]).record(enforce.isNumber()); }).toThrow(); }); it('skips prototype keys', () => { const obj = Object.create({ inherited: 'bad' }); obj.own = 42; expect(() => { enforce(obj).record(enforce.isNumber()); }).not.toThrow(); }); }); describe('key + value validation', () => { it('validates both keys and values', () => { expect(() => { enforce({ abc: 1, def: 2 }).record( enforce.isString().matches(/^[a-z]+$/), enforce.isNumber(), ); }).not.toThrow(); }); it('fails when key does not match', () => { expect(() => { enforce({ INVALID: 1 }).record( enforce.isString().matches(/^[a-z]+$/), enforce.isNumber(), ); }).toThrow(); }); }); }); });