UNPKG

@wearesage/schema

Version:

A flexible schema definition and validation system for TypeScript with multi-database support

234 lines (199 loc) 6.73 kB
import "reflect-metadata"; import * as fc from "fast-check"; import { MetadataRegistry, SchemaBuilder, Entity, Property, Id } from "../../core"; import { SchemaReflector } from "../../core/SchemaReflector"; // Define a basic entity class for testing @Entity() class TestEntity { @Id() id: string; @Property({ required: true }) requiredString: string; @Property({ required: false }) optionalString?: string; @Property({ required: true }) requiredNumber: number; @Property({ required: false }) optionalNumber?: number; @Property({ required: true }) requiredBoolean: boolean; @Property({ required: false }) optionalBoolean?: boolean; @Property({ required: true }) requiredDate: Date; @Property({ required: false }) optionalDate?: Date; } describe("Property-based Validation Tests", () => { let registry: MetadataRegistry; let builder: SchemaBuilder; let reflector: SchemaReflector; beforeEach(() => { registry = new MetadataRegistry(); builder = new SchemaBuilder(registry); reflector = new SchemaReflector(registry); builder.registerEntity(TestEntity); }); test("validation should pass for valid entities", () => { // Define arbitrary generators for each type const idArb = fc.string({ minLength: 1 }); const stringArb = fc.string(); const numberArb = fc.double(); const booleanArb = fc.boolean(); const dateArb = fc.date(); // Define an arbitrary for the whole entity const entityArb = fc.record({ id: idArb, requiredString: stringArb, optionalString: fc.option(stringArb), requiredNumber: numberArb, optionalNumber: fc.option(numberArb), requiredBoolean: booleanArb, optionalBoolean: fc.option(booleanArb), requiredDate: dateArb, optionalDate: fc.option(dateArb), }); // Test 100 different valid entity configurations fc.assert( fc.property(entityArb, (entityData) => { // Create entity instance const entity = new TestEntity(); Object.assign(entity, entityData); // Validate entity const result = reflector.validateEntity(entity); // Should be valid return result.valid && result.errors.length === 0; }), { numRuns: 100 } ); }); test("validation should fail if required properties are missing", () => { // Test for each required field one at a time const requiredFields = ['requiredString', 'requiredNumber', 'requiredBoolean', 'requiredDate']; fc.assert( fc.property(fc.constantFrom(...requiredFields), (fieldToRemove) => { // Create a complete entity const entity = new TestEntity(); entity.id = "123"; entity.requiredString = "test"; entity.requiredNumber = 42; entity.requiredBoolean = true; entity.requiredDate = new Date(); // Remove the specified field delete (entity as any)[fieldToRemove]; // Validate entity const result = reflector.validateEntity(entity); // Should be invalid with exactly one error if (!result.valid && result.errors.length === 1) { // The error should mention the missing field return result.errors[0].includes(fieldToRemove); } return false; }), { numRuns: 10 } ); }); test("validation should handle edge cases for required properties", () => { fc.assert( fc.property( fc.record({ // Generate only empty strings, zero values, false values requiredString: fc.constant(""), requiredNumber: fc.constant(0), requiredBoolean: fc.constant(false), requiredDate: fc.date() }), (values) => { // Create entity with edge-case values const entity = new TestEntity(); entity.id = "123"; Object.assign(entity, values); // Validate entity - empty string, 0, and false are valid values // They should pass validation since they're not undefined or null const result = reflector.validateEntity(entity); return result.valid && result.errors.length === 0; } ), { numRuns: 50 } ); }); test("validation should handle null vs undefined", () => { fc.assert( fc.property( fc.record({ // For all optional fields, test with null optionalString: fc.constant(null), optionalNumber: fc.constant(null), optionalBoolean: fc.constant(null), optionalDate: fc.constant(null), }), (nullValues) => { // Create a complete entity const entity = new TestEntity(); entity.id = "123"; entity.requiredString = "test"; entity.requiredNumber = 42; entity.requiredBoolean = true; entity.requiredDate = new Date(); // Add null values for optional fields Object.assign(entity, nullValues); // Validate entity - null should be treated as missing const result = reflector.validateEntity(entity); // Should still be valid since these are optional fields return result.valid && result.errors.length === 0; } ), { numRuns: 10 } ); }); test("validation should work with complex object structures", () => { // Create a new entity class with nested objects @Entity() class ComplexEntity { @Id() id: string; @Property({ required: true }) metadata: { created: Date; tags: string[]; settings: { enabled: boolean; level: number; } }; } // Register the complex entity builder.registerEntity(ComplexEntity); // Define arbitrary generators for the complex structure const complexEntityArb = fc.record({ id: fc.string({ minLength: 1 }), metadata: fc.record({ created: fc.date(), tags: fc.array(fc.string()), settings: fc.record({ enabled: fc.boolean(), level: fc.integer() }) }) }); // Test validation with complex structures fc.assert( fc.property(complexEntityArb, (entityData) => { // Create entity instance const entity = new ComplexEntity(); Object.assign(entity, entityData); // Validate entity const result = reflector.validateEntity(entity); // Should be valid return result.valid && result.errors.length === 0; }), { numRuns: 50 } ); }); });