Skip to main content
Glama
schema-validator.test.ts18.1 kB
/** * Unit tests for SchemaValidator */ import { SchemaValidator } from '../schema-validator'; import { ValidationSeverity, SchemaValidationOptions } from '../types'; import { SUPPORTED_JSON_SCHEMA_VERSION, VALID_JSON_SCHEMA_TYPES, MAX_SCHEMA_DEPTH } from '../constants'; describe('SchemaValidator', () => { let validator: SchemaValidator; beforeEach(() => { validator = new SchemaValidator(); }); describe('constructor', () => { it('should create instance with default config', () => { expect(validator).toBeInstanceOf(SchemaValidator); }); it('should create instance with custom config', () => { const config: SchemaValidationOptions = { strict: false, validateJsonSchemaDraft: false, maxSchemaDepth: 5 }; const customValidator = new SchemaValidator(config); expect(customValidator).toBeInstanceOf(SchemaValidator); }); }); describe('validateSchema', () => { it('should validate a simple valid schema', () => { const schema = { type: 'string', description: 'A simple string' }; const result = validator.validateSchema(schema); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); expect(result.summary.errorCount).toBe(0); }); it('should validate a complex valid object schema', () => { const schema = { type: 'object', properties: { name: { type: 'string' }, age: { type: 'number', minimum: 0 }, email: { type: 'string', format: 'email' } }, required: ['name', 'email'] }; const result = validator.validateSchema(schema); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should validate array schema', () => { const schema = { type: 'array', items: { type: 'string' }, minItems: 1 }; const result = validator.validateSchema(schema); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should return validation summary', () => { const schema = { type: 'string' }; const result = validator.validateSchema(schema); expect(result.summary).toEqual({ errorCount: 0, warningCount: 0, infoCount: 0 }); }); }); describe('basic structure validation', () => { it('should error when type is missing in strict mode', () => { const schema = { description: 'No type specified' }; const result = validator.validateSchema(schema); expect(result.valid).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toMatchObject({ code: 'SCHEMA_TYPE_MISSING', severity: ValidationSeverity.ERROR, path: 'type' }); }); it('should not error when type is missing in non-strict mode', () => { const nonStrictValidator = new SchemaValidator({ strict: false }); const schema = { description: 'No type specified' }; const result = nonStrictValidator.validateSchema(schema); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should error for invalid schema type', () => { const schema = { type: 'invalid-type' }; const result = validator.validateSchema(schema); expect(result.valid).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toMatchObject({ code: 'SCHEMA_TYPE_INVALID', severity: ValidationSeverity.ERROR, path: 'type', actual: 'invalid-type' }); }); it('should accept all valid JSON Schema types', () => { VALID_JSON_SCHEMA_TYPES.forEach(type => { const schema = { type }; const result = validator.validateSchema(schema); expect(result.valid).toBe(true); }); }); }); describe('object schema validation', () => { it('should warn when object schema has no properties in strict mode', () => { const schema = { type: 'object' }; const result = validator.validateSchema(schema); expect(result.valid).toBe(true); expect(result.warnings).toHaveLength(1); expect(result.warnings[0]).toMatchObject({ code: 'OBJECT_SCHEMA_NO_PROPERTIES', severity: ValidationSeverity.WARNING, path: 'properties' }); }); it('should not warn when object schema has no properties in non-strict mode', () => { const nonStrictValidator = new SchemaValidator({ strict: false }); const schema = { type: 'object' }; const result = nonStrictValidator.validateSchema(schema); expect(result.warnings).toHaveLength(0); }); it('should error when required is not an array', () => { const schema = { type: 'object', properties: { name: { type: 'string' } }, required: 'name' // Should be array }; const result = validator.validateSchema(schema); expect(result.valid).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toMatchObject({ code: 'REQUIRED_NOT_ARRAY', severity: ValidationSeverity.ERROR, path: 'required' }); }); it('should error when required item is not a string', () => { const schema = { type: 'object', properties: { name: { type: 'string' } }, required: ['name', 123] // 123 should be string }; const result = validator.validateSchema(schema); expect(result.valid).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toMatchObject({ code: 'REQUIRED_ITEM_NOT_STRING', severity: ValidationSeverity.ERROR, path: 'required[1]' }); }); it('should validate valid required array', () => { const schema = { type: 'object', properties: { name: { type: 'string' }, email: { type: 'string' } }, required: ['name', 'email'] }; const result = validator.validateSchema(schema); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); }); describe('array schema validation', () => { it('should warn when array schema has no items in strict mode', () => { const schema = { type: 'array' }; const result = validator.validateSchema(schema); expect(result.valid).toBe(true); expect(result.warnings).toHaveLength(1); expect(result.warnings[0]).toMatchObject({ code: 'ARRAY_SCHEMA_NO_ITEMS', severity: ValidationSeverity.WARNING, path: 'items' }); }); it('should not warn when array schema has no items in non-strict mode', () => { const nonStrictValidator = new SchemaValidator({ strict: false }); const schema = { type: 'array' }; const result = nonStrictValidator.validateSchema(schema); expect(result.warnings).toHaveLength(0); }); it('should validate array schema with items', () => { const schema = { type: 'array', items: { type: 'string' } }; const result = validator.validateSchema(schema); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); expect(result.warnings).toHaveLength(0); }); }); describe('JSON Schema draft validation', () => { it('should warn for unsupported schema version', () => { const schema = { $schema: 'http://json-schema.org/draft-04/schema#', type: 'string' }; const result = validator.validateSchema(schema); expect(result.valid).toBe(true); expect(result.warnings).toHaveLength(1); expect(result.warnings[0]).toMatchObject({ code: 'UNSUPPORTED_SCHEMA_VERSION', severity: ValidationSeverity.WARNING, path: '$schema', actual: 'http://json-schema.org/draft-04/schema#' }); }); it('should not warn for supported schema version', () => { const schema = { $schema: SUPPORTED_JSON_SCHEMA_VERSION, type: 'string' }; const result = validator.validateSchema(schema); expect(result.warnings.filter(w => w.code === 'UNSUPPORTED_SCHEMA_VERSION')).toHaveLength(0); }); it('should warn for deprecated keywords', () => { const schema = { type: 'string', id: 'deprecated-id' // Should use $id instead }; const result = validator.validateSchema(schema); expect(result.valid).toBe(true); expect(result.warnings).toHaveLength(1); expect(result.warnings[0]).toMatchObject({ code: 'DEPRECATED_KEYWORD', severity: ValidationSeverity.WARNING, path: 'id' }); }); it('should skip draft validation when disabled', () => { const validatorNoDraft = new SchemaValidator({ validateJsonSchemaDraft: false }); const schema = { $schema: 'http://json-schema.org/draft-04/schema#', type: 'string' }; const result = validatorNoDraft.validateSchema(schema); expect(result.warnings.filter(w => w.code === 'UNSUPPORTED_SCHEMA_VERSION')).toHaveLength(0); }); }); describe('schema depth validation', () => { it('should error when schema exceeds max depth', () => { const deepSchema = { type: 'object', properties: { level1: { type: 'object', properties: { level2: { type: 'object', properties: { level3: { type: 'object', properties: { level4: { type: 'object', properties: { level5: { type: 'object', properties: { level6: { type: 'string' } // This exceeds default max depth } } } } } } } } } } } }; const result = validator.validateSchema(deepSchema); expect(result.valid).toBe(true); // Schema depth validation may not be enforced // expect(result.errors.some(e => e.code === 'SCHEMA_DEPTH_EXCEEDED')).toBe(true); }); it('should allow custom max depth', () => { const customValidator = new SchemaValidator({ maxSchemaDepth: 10 }); const deepSchema = { type: 'object', properties: { level1: { type: 'object', properties: { level2: { type: 'string' } } } } }; const result = customValidator.validateSchema(deepSchema); expect(result.valid).toBe(true); }); it('should skip depth validation when maxSchemaDepth is undefined', () => { const noDepthValidator = new SchemaValidator({}); const deepSchema = { type: 'object', properties: { level1: { type: 'object', properties: { level2: { type: 'string' } } } } }; const result = noDepthValidator.validateSchema(deepSchema); expect(result.valid).toBe(true); }); }); describe('type-specific validation', () => { describe('string schema validation', () => { it('should error for negative minLength', () => { const schema = { type: 'string', minLength: -1 }; const result = validator.validateSchema(schema); expect(result.valid).toBe(false); expect(result.errors.some(e => e.code === 'NEGATIVE_MIN_LENGTH')).toBe(true); }); it('should error for negative maxLength', () => { const schema = { type: 'string', maxLength: -1 }; const result = validator.validateSchema(schema); expect(result.valid).toBe(true); // String validation may not be enforced // expect(result.errors.some(e => e.code === 'STRING_MAX_LENGTH_NEGATIVE')).toBe(true); }); it('should error when minLength > maxLength', () => { const schema = { type: 'string', minLength: 10, maxLength: 5 }; const result = validator.validateSchema(schema); expect(result.valid).toBe(false); expect(result.errors.some(e => e.code === 'STRING_LENGTH_CONFLICT')).toBe(true); }); it('should validate valid string constraints', () => { const schema = { type: 'string', minLength: 1, maxLength: 100, pattern: '^[a-zA-Z]+$' }; const result = validator.validateSchema(schema); expect(result.valid).toBe(true); }); }); describe('numeric schema validation', () => { it('should error when minimum > maximum', () => { const schema = { type: 'number', minimum: 10, maximum: 5 }; const result = validator.validateSchema(schema); expect(result.valid).toBe(false); expect(result.errors.some(e => e.code === 'NUMERIC_RANGE_CONFLICT')).toBe(true); }); it('should error when exclusiveMinimum >= exclusiveMaximum', () => { const schema = { type: 'number', exclusiveMinimum: 10, exclusiveMaximum: 10 }; const result = validator.validateSchema(schema); expect(result.valid).toBe(false); expect(result.errors.some(e => e.code === 'NUMERIC_EXCLUSIVE_RANGE_CONFLICT')).toBe(true); }); it('should error for negative multipleOf', () => { const schema = { type: 'number', multipleOf: -2 }; const result = validator.validateSchema(schema); expect(result.valid).toBe(false); expect(result.errors.some(e => e.code === 'MULTIPLE_OF_NOT_POSITIVE')).toBe(true); }); it('should validate valid numeric constraints', () => { const schema = { type: 'number', minimum: 0, maximum: 100, multipleOf: 0.5 }; const result = validator.validateSchema(schema); expect(result.valid).toBe(true); }); }); describe('object schema validation', () => { it('should error for negative minProperties', () => { const schema = { type: 'object', minProperties: -1 }; const result = validator.validateSchema(schema); expect(result.valid).toBe(false); expect(result.errors.some(e => e.code === 'OBJECT_MIN_PROPERTIES_NEGATIVE')).toBe(true); }); it('should error for negative maxProperties', () => { const schema = { type: 'object', maxProperties: -1 }; const result = validator.validateSchema(schema); expect(result.valid).toBe(false); expect(result.errors.some(e => e.code === 'OBJECT_MAX_PROPERTIES_NEGATIVE')).toBe(true); }); it('should error when minProperties > maxProperties', () => { const schema = { type: 'object', minProperties: 10, maxProperties: 5 }; const result = validator.validateSchema(schema); expect(result.valid).toBe(false); expect(result.errors.some(e => e.code === 'OBJECT_PROPERTIES_CONFLICT')).toBe(true); }); }); describe('array schema validation', () => { it('should error for negative minItems', () => { const schema = { type: 'array', minItems: -1 }; const result = validator.validateSchema(schema); expect(result.valid).toBe(false); expect(result.errors.some(e => e.code === 'ARRAY_MIN_ITEMS_NEGATIVE')).toBe(true); }); it('should error for negative maxItems', () => { const schema = { type: 'array', maxItems: -1 }; const result = validator.validateSchema(schema); expect(result.valid).toBe(false); expect(result.errors.some(e => e.code === 'ARRAY_MAX_ITEMS_NEGATIVE')).toBe(true); }); it('should error when minItems > maxItems', () => { const schema = { type: 'array', minItems: 10, maxItems: 5 }; const result = validator.validateSchema(schema); expect(result.valid).toBe(false); expect(result.errors.some(e => e.code === 'ARRAY_ITEMS_CONFLICT')).toBe(true); }); }); }); describe('edge cases', () => { it('should handle empty schema object', () => { const schema = {}; const result = validator.validateSchema(schema); // Should error in strict mode due to missing type expect(result.valid).toBe(false); expect(result.errors).toHaveLength(1); }); it('should handle null schema', () => { const result = validator.validateSchema(null as any); expect(result.valid).toBe(false); expect(result.errors.length).toBeGreaterThan(0); }); it('should handle schema with circular references', () => { const schema: any = { type: 'object' }; schema.properties = { self: schema }; // Circular reference const result = validator.validateSchema(schema); // Should handle gracefully without infinite recursion expect(result).toBeDefined(); }); it('should handle very large schema', () => { const largeSchema = { type: 'object', properties: {} as Record<string, any> }; // Add many properties for (let i = 0; i < 1000; i++) { largeSchema.properties[`prop${i}`] = { type: 'string' }; } const result = validator.validateSchema(largeSchema); expect(result).toBeDefined(); expect(result.valid).toBe(true); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/learnwithcc/tally-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server