@sailboat-computer/event-bus
Version:
Standardized event bus for sailboat computer v3 with resilience features and offline capabilities
354 lines (302 loc) • 9.84 kB
text/typescript
/**
* Validation tests
*/
import { SchemaRegistry, createSchemaRegistry, ValidationResult } from '../../src/validation';
import { EventEnvelope, EventPriority, EventCategory } from '../../src/types';
describe('SchemaRegistry', () => {
let registry: SchemaRegistry;
beforeEach(() => {
registry = createSchemaRegistry();
});
describe('registerSchema', () => {
it('should register a schema', () => {
const schema = {
type: 'object',
required: ['id', 'name'],
properties: {
id: { type: 'string' },
name: { type: 'string' },
age: { type: 'number' }
}
};
registry.registerSchema('test.event.v1', schema);
expect(registry.hasSchema('test.event.v1')).toBe(true);
expect(registry.getSchema('test.event.v1')).toEqual(schema);
});
it('should overwrite an existing schema', () => {
const schema1 = {
type: 'object',
required: ['id', 'name'],
properties: {
id: { type: 'string' },
name: { type: 'string' }
}
};
const schema2 = {
type: 'object',
required: ['id', 'name', 'age'],
properties: {
id: { type: 'string' },
name: { type: 'string' },
age: { type: 'number' }
}
};
registry.registerSchema('test.event.v1', schema1);
registry.registerSchema('test.event.v1', schema2);
expect(registry.getSchema('test.event.v1')).toEqual(schema2);
});
});
describe('hasSchema', () => {
it('should return true if schema exists', () => {
const schema = {
type: 'object',
required: ['id'],
properties: {
id: { type: 'string' }
}
};
registry.registerSchema('test.event.v1', schema);
expect(registry.hasSchema('test.event.v1')).toBe(true);
});
it('should return false if schema does not exist', () => {
expect(registry.hasSchema('test.event.v1')).toBe(false);
});
});
describe('getSchema', () => {
it('should return the schema if it exists', () => {
const schema = {
type: 'object',
required: ['id'],
properties: {
id: { type: 'string' }
}
};
registry.registerSchema('test.event.v1', schema);
expect(registry.getSchema('test.event.v1')).toEqual(schema);
});
it('should return null if schema does not exist', () => {
expect(registry.getSchema('test.event.v1')).toBeNull();
});
});
describe('validate', () => {
beforeEach(() => {
const userSchema = {
type: 'object',
required: ['id', 'username', 'email'],
properties: {
id: { type: 'string' },
username: { type: 'string', minLength: 3 },
email: { type: 'string', format: 'email' },
age: { type: 'number', minimum: 0 }
},
additionalProperties: false
};
registry.registerSchema('user.created.v1', userSchema);
});
it('should validate valid data', () => {
const event: EventEnvelope = {
id: 'event-123',
type: 'user.created.v1',
timestamp: new Date(),
source: 'test-service',
version: '1.0',
data: {
id: 'user-123',
username: 'sailor',
email: 'sailor@example.com',
age: 30
},
metadata: {
priority: EventPriority.NORMAL,
category: EventCategory.DATA
}
};
const result = registry.validate(event);
expect(result.valid).toBe(true);
expect(result.errors).toBeNull();
});
it('should validate valid data without optional fields', () => {
const event: EventEnvelope = {
id: 'event-123',
type: 'user.created.v1',
timestamp: new Date(),
source: 'test-service',
version: '1.0',
data: {
id: 'user-123',
username: 'sailor',
email: 'sailor@example.com'
},
metadata: {
priority: EventPriority.NORMAL,
category: EventCategory.DATA
}
};
const result = registry.validate(event);
expect(result.valid).toBe(true);
expect(result.errors).toBeNull();
});
it('should invalidate data with missing required fields', () => {
const event: EventEnvelope = {
id: 'event-123',
type: 'user.created.v1',
timestamp: new Date(),
source: 'test-service',
version: '1.0',
data: {
id: 'user-123',
username: 'sailor'
// Missing email
},
metadata: {
priority: EventPriority.NORMAL,
category: EventCategory.DATA
}
};
const result = registry.validate(event);
expect(result.valid).toBe(false);
expect(result.errors).not.toBeNull();
expect(result.errors!.length).toBeGreaterThan(0);
expect(result.errors![0].message).toContain('email');
});
it('should invalidate data with invalid field types', () => {
const event: EventEnvelope = {
id: 'event-123',
type: 'user.created.v1',
timestamp: new Date(),
source: 'test-service',
version: '1.0',
data: {
id: 'user-123',
username: 'sailor',
email: 'sailor@example.com',
age: 'thirty' as any // Should be a number
},
metadata: {
priority: EventPriority.NORMAL,
category: EventCategory.DATA
}
};
const result = registry.validate(event);
expect(result.valid).toBe(false);
expect(result.errors).not.toBeNull();
expect(result.errors!.length).toBeGreaterThan(0);
expect(result.errors![0].message).toContain('number');
});
it('should invalidate data with additional properties', () => {
const event: EventEnvelope = {
id: 'event-123',
type: 'user.created.v1',
timestamp: new Date(),
source: 'test-service',
version: '1.0',
data: {
id: 'user-123',
username: 'sailor',
email: 'sailor@example.com',
role: 'admin' // Not in schema
} as any,
metadata: {
priority: EventPriority.NORMAL,
category: EventCategory.DATA
}
};
const result = registry.validate(event);
expect(result.valid).toBe(false);
expect(result.errors).not.toBeNull();
expect(result.errors!.length).toBeGreaterThan(0);
expect(result.errors![0].message).toContain('additional properties');
});
it('should invalidate data with invalid format', () => {
const event: EventEnvelope = {
id: 'event-123',
type: 'user.created.v1',
timestamp: new Date(),
source: 'test-service',
version: '1.0',
data: {
id: 'user-123',
username: 'sailor',
email: 'not-an-email' // Invalid email format
},
metadata: {
priority: EventPriority.NORMAL,
category: EventCategory.DATA
}
};
const result = registry.validate(event);
expect(result.valid).toBe(false);
expect(result.errors).not.toBeNull();
expect(result.errors!.length).toBeGreaterThan(0);
expect(result.errors![0].message).toContain('email');
});
it('should invalidate data with value outside constraints', () => {
const event: EventEnvelope = {
id: 'event-123',
type: 'user.created.v1',
timestamp: new Date(),
source: 'test-service',
version: '1.0',
data: {
id: 'user-123',
username: 'sa', // Too short
email: 'sailor@example.com'
},
metadata: {
priority: EventPriority.NORMAL,
category: EventCategory.DATA
}
};
const result = registry.validate(event);
expect(result.valid).toBe(false);
expect(result.errors).not.toBeNull();
expect(result.errors!.length).toBeGreaterThan(0);
expect(result.errors![0].message).toContain('shorter than 3');
});
});
describe('removeSchema', () => {
it('should remove a schema', () => {
const schema = {
type: 'object',
required: ['id'],
properties: {
id: { type: 'string' }
}
};
registry.registerSchema('test.event.v1', schema);
expect(registry.hasSchema('test.event.v1')).toBe(true);
registry.removeSchema('test.event.v1');
expect(registry.hasSchema('test.event.v1')).toBe(false);
});
it('should do nothing if schema does not exist', () => {
expect(() => {
registry.removeSchema('non.existent.schema');
}).not.toThrow();
});
});
describe('clearSchemas', () => {
it('should clear all schemas', () => {
const schema1 = {
type: 'object',
required: ['id'],
properties: {
id: { type: 'string' }
}
};
const schema2 = {
type: 'object',
required: ['name'],
properties: {
name: { type: 'string' }
}
};
registry.registerSchema('test.event.v1', schema1);
registry.registerSchema('test.event.v2', schema2);
expect(registry.hasSchema('test.event.v1')).toBe(true);
expect(registry.hasSchema('test.event.v2')).toBe(true);
registry.clearSchemas();
expect(registry.hasSchema('test.event.v1')).toBe(false);
expect(registry.hasSchema('test.event.v2')).toBe(false);
});
});
});