@wearesage/schema
Version:
A flexible schema definition and validation system for TypeScript with multi-database support
234 lines (199 loc) • 6.73 kB
text/typescript
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
()
class TestEntity {
()
id: string;
({ required: true })
requiredString: string;
({ required: false })
optionalString?: string;
({ required: true })
requiredNumber: number;
({ required: false })
optionalNumber?: number;
({ required: true })
requiredBoolean: boolean;
({ required: false })
optionalBoolean?: boolean;
({ required: true })
requiredDate: Date;
({ 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
()
class ComplexEntity {
()
id: string;
({ 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 }
);
});
});