n4s
Version:
typed schema validation version of enforce
831 lines (756 loc) • 23.7 kB
text/typescript
import { isNullish, isObject } from 'vest-utils';
import { describe, it, expect, beforeEach } from 'vitest';
import { enforce } from '../n4s';
type ContextRuleResult =
| boolean
| { pass: boolean; message?: string | (() => string) };
declare global {
namespace n4s {
interface EnforceMatchers {
hasContext: (_value: unknown) => ContextRuleResult;
checkContextStructure: (_value: unknown) => ContextRuleResult;
checkValue: (_value: unknown) => ContextRuleResult;
checkMetaName: (
_value: unknown,
_expectedName: string,
) => ContextRuleResult;
checkMetaIndex: (
_value: unknown,
_expectedIndex: number,
) => ContextRuleResult;
hasMetaIndex: (_value: unknown) => ContextRuleResult;
hasParent: (_value: unknown) => ContextRuleResult;
canAccessParent: (_value: unknown) => ContextRuleResult;
matchesParentUsername: (_value: string) => ContextRuleResult;
differentFromParentUsername: (_value: string) => ContextRuleResult;
isFriendTheSameAsUser: (_value: string) => ContextRuleResult;
emailMatchesUsername: (_value: string) => ContextRuleResult;
matchesTopLevelId: (_value: string) => ContextRuleResult;
notSameAsSibling: (
_value: string,
_siblingKey: string,
) => ContextRuleResult;
uniqueInArray: (_value: unknown) => ContextRuleResult;
passwordsMatch: (_value: string) => ContextRuleResult;
checkParentIsNull: (_value: unknown) => ContextRuleResult;
requiredIfOtherFieldPresent: (
_value: unknown,
_otherField: string,
) => ContextRuleResult;
differentFromUsername: (_value: string) => ContextRuleResult;
minAgeIfCountry: (
_value: number,
_country: string,
_minAge: number,
) => ContextRuleResult;
matchesParentId: (_value: string) => ContextRuleResult;
safeParentAccess: (_value: unknown) => ContextRuleResult;
arrayLengthMatchesCount: (_value: any[]) => ContextRuleResult;
}
}
}
describe('enforce.context() API', () => {
describe('Basic context access', () => {
beforeEach(() => {
enforce.extend({
hasContext: () => {
const context = enforce.context();
return !isNullish(context);
},
});
});
it('should provide context within custom rules', () => {
expect(() => enforce('test').hasContext()).not.toThrow();
});
// Context is not available in lazy mode (when building rule chain)
it('should work in lazy mode', () => {
const result = enforce.hasContext().run('test');
expect(result.pass).toBe(true);
});
});
describe('Context structure', () => {
beforeEach(() => {
enforce.extend({
checkContextStructure: () => {
const context = enforce.context();
return (
isObject(context) &&
'value' in context &&
'meta' in context &&
'parent' in context
);
},
});
});
it('should have value, meta, and parent properties', () => {
expect(() => enforce('test').checkContextStructure()).not.toThrow();
});
});
describe('Context value property', () => {
beforeEach(() => {
enforce.extend({
checkValue: (value: any) => {
const context = enforce.context();
return context?.value === value;
},
});
});
it('should contain the current value being validated', () => {
expect(() => enforce('test').checkValue()).not.toThrow();
expect(() => enforce(123).checkValue()).not.toThrow();
expect(() => enforce({ key: 'value' }).checkValue()).not.toThrow();
});
it('should work with different data types', () => {
const testCases = [
'string',
123,
true,
null,
undefined,
{ key: 'value' },
[1, 2, 3],
];
testCases.forEach(testCase => {
expect(() => enforce(testCase).checkValue()).not.toThrow();
});
});
});
describe('Context meta property within shape', () => {
beforeEach(() => {
enforce.extend({
checkMetaName: (_value: any, expectedName: string) => {
const context = enforce.context();
return context?.meta?.key === expectedName;
},
});
});
// Context meta within schema rules is not yet fully implemented
it('should contain the property name when used in shape', () => {
expect(() =>
enforce({
username: 'johndoe',
}).shape({
username: enforce.isString().checkMetaName('username'),
}),
).not.toThrow();
});
// Context meta within schema rules is not yet fully implemented
it('should work with multiple properties', () => {
expect(() =>
enforce({
firstName: 'John',
lastName: 'Doe',
age: 30,
}).shape({
firstName: enforce.isString().checkMetaName('firstName'),
lastName: enforce.isString().checkMetaName('lastName'),
age: enforce.isNumber().checkMetaName('age'),
}),
).not.toThrow();
});
// Context is not available in lazy mode
it('should work in lazy mode', () => {
const schema = enforce.shape({
username: enforce.isString().checkMetaName('username'),
});
const result = schema.run({ username: 'johndoe' });
expect(result.pass).toBe(true);
});
});
describe('Context meta property within isArrayOf', () => {
beforeEach(() => {
enforce.extend({
checkMetaIndex: (_value: any, expectedIndex: number) => {
const context = enforce.context();
return context?.meta?.index === expectedIndex;
},
hasMetaIndex: () => {
const context = enforce.context();
return typeof context?.meta?.index === 'number';
},
});
});
it('should contain the array index when used in isArrayOf', () => {
expect(() =>
enforce({
items: ['first', 'second', 'third'],
}).shape({
items: enforce.isArrayOf(enforce.isString().hasMetaIndex()),
}),
).not.toThrow();
});
it('should have correct index values', () => {
// Note: This test checks that meta.index exists, but checking specific
// index values would require multiple custom rules or advanced logic
expect(() =>
enforce(['a', 'b', 'c']).isArrayOf(enforce.isString().hasMetaIndex()),
).not.toThrow();
});
});
describe('Context parent traversal', () => {
beforeEach(() => {
enforce.extend({
hasParent: () => {
const context = enforce.context();
return typeof context?.parent === 'function';
},
canAccessParent: () => {
const context = enforce.context();
const parent = context?.parent();
return parent !== null;
},
});
});
it('should provide parent function in context', () => {
expect(() =>
enforce({
nested: {
value: 'test',
},
}).shape({
nested: enforce.shape({
value: enforce.isString().hasParent(),
}),
}),
).not.toThrow();
});
it('should allow accessing parent context', () => {
expect(() =>
enforce({
nested: {
value: 'test',
},
}).shape({
nested: enforce.shape({
value: enforce.isString().canAccessParent(),
}),
}),
).not.toThrow();
});
});
describe('Accessing parent values - single level', () => {
beforeEach(() => {
enforce.extend({
matchesParentUsername: (value: string) => {
const context = enforce.context();
const parent = context?.parent();
return value === parent?.value.username;
},
differentFromParentUsername: (value: string) => {
const context = enforce.context();
const parent = context?.parent();
return value !== parent?.value.username;
},
});
});
it('should access parent value for validation', () => {
expect(() =>
enforce({
username: 'johndoe',
displayName: 'johndoe',
}).shape({
username: enforce.isString(),
displayName: enforce.isString().matchesParentUsername(),
}),
).not.toThrow();
});
it('should fail when parent value does not match', () => {
expect(() =>
enforce({
username: 'johndoe',
displayName: 'different',
}).shape({
username: enforce.isString(),
displayName: enforce.isString().matchesParentUsername(),
}),
).toThrow();
});
it('should validate that value is different from parent property', () => {
expect(() =>
enforce({
username: 'johndoe',
displayName: 'different',
}).shape({
username: enforce.isString(),
displayName: enforce.isString().differentFromParentUsername(),
}),
).not.toThrow();
});
});
describe('Accessing parent values - multiple levels (Documentation example)', () => {
beforeEach(() => {
enforce.extend({
isFriendTheSameAsUser: (value: string) => {
const context = enforce.context();
if (value === context?.parent()?.parent()?.value.username) {
return { pass: false };
}
return true;
},
});
});
it('should traverse two levels up to access username', () => {
expect(() =>
enforce({
username: 'johndoe',
friends: ['Mike', 'Jim'],
}).shape({
username: enforce.isString(),
friends: enforce.isArrayOf(
enforce.isString().isFriendTheSameAsUser(),
),
}),
).not.toThrow();
});
it('should fail when friend name matches username', () => {
expect(() =>
enforce({
username: 'johndoe',
friends: ['Mike', 'Jim', 'johndoe'],
}).shape({
username: enforce.isString(),
friends: enforce.isArrayOf(
enforce.isString().isFriendTheSameAsUser(),
),
}),
).toThrow();
});
it('should work with empty friends array', () => {
expect(() =>
enforce({
username: 'johndoe',
friends: [],
}).shape({
username: enforce.isString(),
friends: enforce.isArrayOf(
enforce.isString().isFriendTheSameAsUser(),
),
}),
).not.toThrow();
});
});
describe('Complex nested object validation with context', () => {
beforeEach(() => {
enforce.extend({
emailMatchesUsername: (value: string) => {
const context = enforce.context();
const parent = context?.parent();
const username = parent?.value.username;
// Email should start with username
return value.startsWith(username + '@');
},
});
});
it('should validate email based on username in parent', () => {
expect(() =>
enforce({
username: 'john',
email: 'john@example.com',
}).shape({
username: enforce.isString(),
email: enforce.isString().emailMatchesUsername(),
}),
).not.toThrow();
});
it('should fail when email does not match username pattern', () => {
expect(() =>
enforce({
username: 'john',
email: 'jane@example.com',
}).shape({
username: enforce.isString(),
email: enforce.isString().emailMatchesUsername(),
}),
).toThrow();
});
});
describe('Deeply nested context traversal', () => {
beforeEach(() => {
enforce.extend({
matchesTopLevelId: (value: string) => {
const context = enforce.context();
// Traverse up to the top-level (four levels from leaf)
const topLevel = context?.parent()?.parent()?.parent()?.parent()
?.value.id;
return value === topLevel;
},
});
});
it('should access deeply nested parent values', () => {
expect(() =>
enforce({
id: 'top-level-id',
level1: {
level2: {
level3: {
reference: 'top-level-id',
},
},
},
}).shape({
id: enforce.isString(),
level1: enforce.shape({
level2: enforce.shape({
level3: enforce.shape({
reference: enforce.isString().matchesTopLevelId(),
}),
}),
}),
}),
).not.toThrow();
});
it('should fail when deeply nested value does not match', () => {
expect(() =>
enforce({
id: 'top-level-id',
level1: {
level2: {
level3: {
reference: 'different-id',
},
},
},
}).shape({
id: enforce.isString(),
level1: enforce.shape({
level2: enforce.shape({
level3: enforce.shape({
reference: enforce.isString().matchesTopLevelId(),
}),
}),
}),
}),
).toThrow();
});
});
describe('Context with custom error messages', () => {
beforeEach(() => {
enforce.extend({
notSameAsSibling: (value: string, siblingKey: string) => {
const context = enforce.context();
const parent = context?.parent();
const siblingValue = parent?.value[siblingKey];
if (value === siblingValue) {
return {
pass: false,
message: () =>
`Value "${value}" cannot be the same as ${siblingKey}`,
};
}
return true;
},
});
});
it('should provide custom error message with context info', () => {
expect(() =>
enforce({
password: 'secret',
username: 'different',
}).shape({
password: enforce.isString(),
username: enforce.isString().notSameAsSibling('password'),
}),
).not.toThrow();
});
it('should show custom error when values match', () => {
expect(() =>
enforce({
password: 'same',
username: 'same',
}).shape({
password: enforce.isString(),
username: enforce.isString().notSameAsSibling('password'),
}),
).toThrow('Value "same" cannot be the same as password');
});
});
describe('Context in array validation scenarios', () => {
beforeEach(() => {
enforce.extend({
uniqueInArray: (value: any) => {
const context = enforce.context();
const parent = context?.parent();
const array = parent?.value;
if (!Array.isArray(array)) return true;
const occurrences = array.filter(
(item: any) => item === value,
).length;
return occurrences === 1;
},
});
});
it('should validate uniqueness within array using context', () => {
expect(() =>
enforce({
tags: ['javascript', 'typescript', 'node'],
}).shape({
tags: enforce.isArrayOf(enforce.isString().uniqueInArray()),
}),
).not.toThrow();
});
it('should fail when array has duplicates', () => {
expect(() =>
enforce({
tags: ['javascript', 'typescript', 'javascript'],
}).shape({
tags: enforce.isArrayOf(enforce.isString().uniqueInArray()),
}),
).toThrow();
});
});
describe('Context with password confirmation pattern', () => {
beforeEach(() => {
enforce.extend({
passwordsMatch: (passConfirm: string) => {
const context = enforce.context();
const parent = context?.parent();
const password = parent?.value.password;
return passConfirm === password;
},
});
});
it('should validate password confirmation matches password', () => {
expect(() =>
enforce({
password: 'SecurePass123',
confirmPassword: 'SecurePass123',
}).shape({
password: enforce.isString(),
confirmPassword: enforce.isString().passwordsMatch(),
}),
).not.toThrow();
});
it('should fail when passwords do not match', () => {
expect(() =>
enforce({
password: 'SecurePass123',
confirmPassword: 'Different123',
}).shape({
password: enforce.isString(),
confirmPassword: enforce.isString().passwordsMatch(),
}),
).toThrow();
});
// Context is not available in lazy mode
it('should work in lazy mode', () => {
const schema = enforce.shape({
password: enforce.isString(),
confirmPassword: enforce.isString().passwordsMatch(),
});
expect(
schema.run({
password: 'SecurePass123',
confirmPassword: 'SecurePass123',
}).pass,
).toBe(true);
expect(
schema.run({
password: 'SecurePass123',
confirmPassword: 'Different123',
}).pass,
).toBe(false);
});
});
describe('Context at top level (no parent)', () => {
beforeEach(() => {
enforce.extend({
checkParentIsNull: () => {
const context = enforce.context();
const parent = context?.parent();
// At top level, parent should return null
return parent === null;
},
});
});
it('should return null when calling parent at top level', () => {
// This test verifies the documentation statement:
// "When no levels left, parent will return null"
expect(() => enforce('test').checkParentIsNull()).not.toThrow();
});
});
describe('Context with conditional validation', () => {
beforeEach(() => {
enforce.extend({
requiredIfOtherFieldPresent: (value: any, otherField: string) => {
const context = enforce.context();
const parent = context?.parent();
const otherFieldValue = parent?.value[otherField];
// If other field is present, this field is required
if (otherFieldValue !== undefined && otherFieldValue !== null) {
return value !== undefined && value !== null && value !== '';
}
return true;
},
});
});
it('should require field when other field is present', () => {
expect(() =>
enforce({
hasShipping: true,
shippingAddress: '123 Main St',
}).loose({
hasShipping: enforce.isBoolean(),
shippingAddress: enforce
.isString()
.requiredIfOtherFieldPresent('hasShipping'),
}),
).not.toThrow();
});
it('should not require field when other field is absent', () => {
expect(() =>
enforce({
hasShipping: false,
}).loose({
hasShipping: enforce.optional(enforce.isBoolean()),
shippingAddress: enforce
.optional(enforce.isString())
.requiredIfOtherFieldPresent('i_am_missing'),
}),
).not.toThrow();
});
});
describe('Real-world form validation scenarios', () => {
beforeEach(() => {
enforce.extend({
differentFromUsername: (value: string) => {
const context = enforce.context();
const parent = context?.parent();
return value !== parent?.value.username;
},
minAgeIfCountry: (value: number, country: string, minAge: number) => {
const context = enforce.context();
const parent = context?.parent();
if (parent?.value.country === country) {
return value >= minAge;
}
return true;
},
});
});
it('should validate user registration form', () => {
expect(() =>
enforce({
username: 'johndoe',
email: 'john@example.com',
displayName: 'John Doe',
}).shape({
username: enforce.isString().longerThan(3),
email: enforce.isString().matches(/@/),
displayName: enforce.isString().differentFromUsername(),
}),
).not.toThrow();
});
it('should validate age based on country context', () => {
expect(() =>
enforce({
country: 'US',
age: 21,
}).shape({
country: enforce.isString(),
age: enforce.isNumber().minAgeIfCountry('US', 21),
}),
).not.toThrow();
});
it('should fail age validation for specific country', () => {
expect(() =>
enforce({
country: 'US',
age: 18,
}).shape({
country: enforce.isString(),
age: enforce.isNumber().minAgeIfCountry('US', 21),
}),
).toThrow();
});
});
describe('Context with loose schema', () => {
beforeEach(() => {
enforce.extend({
matchesParentId: (value: string) => {
const context = enforce.context();
const parent = context?.parent();
return value === parent?.value.id;
},
});
});
it('should work with loose schema allowing extra properties', () => {
expect(() =>
enforce({
id: '123',
reference: '123',
extra: 'field',
}).loose({
id: enforce.isString(),
reference: enforce.isString().matchesParentId(),
}),
).not.toThrow();
});
});
describe('Edge cases and error handling', () => {
beforeEach(() => {
enforce.extend({
safeParentAccess: () => {
const context = enforce.context();
// Try to access parent safely
try {
const parent = context?.parent();
return parent !== undefined;
} catch (e) {
return false;
}
},
});
});
it('should handle safe parent access', () => {
expect(() =>
enforce({
nested: {
value: 'test',
},
}).shape({
nested: enforce.shape({
value: enforce.isString().safeParentAccess(),
}),
}),
).not.toThrow();
});
});
describe('Integration with other schema rules', () => {
beforeEach(() => {
enforce.extend({
arrayLengthMatchesCount: (value: any[]) => {
const context = enforce.context();
const parent = context?.parent();
const count = parent?.value.count;
return value.length === count;
},
});
});
it('should validate array length based on sibling property', () => {
expect(() =>
enforce({
count: 3,
items: ['a', 'b', 'c'],
}).shape({
count: enforce.isNumber(),
items: enforce
.isArrayOf(enforce.isString())
.arrayLengthMatchesCount(),
}),
).not.toThrow();
});
it('should fail when array length does not match count', () => {
expect(() =>
enforce({
count: 2,
items: ['a', 'b', 'c'],
}).shape({
count: enforce.isNumber(),
items: enforce
.isArrayOf(enforce.isString())
.arrayLengthMatchesCount(),
}),
).toThrow();
});
});
});