/**
* Unit Tests for Schema Validator
*
* Tests for JSON schema validation.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { SchemaValidator } from '../../src/mcp/validation/schema-validator.js';
import * as toolsManifest from '../../src/config/tools-manifest.json' assert { type: 'json' };
describe('Schema Validator', () => {
let validator;
beforeEach(() => {
validator = new SchemaValidator();
});
describe('initializeSchemas', () => {
it('should initialize schemas from tools manifest', () => {
expect(validator.schemas.size).toBeGreaterThan(0);
expect(validator.validators.size).toBeGreaterThan(0);
});
});
describe('validate', () => {
describe('list_repositories', () => {
it('should validate valid params', async () => {
const result = await validator.validate('list_repositories', {
perPage: 30,
page: 1,
visibility: 'all'
});
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should reject invalid perPage', async () => {
const result = await validator.validate('list_repositories', {
perPage: 200 // Exceeds maximum
});
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
});
it('should reject invalid visibility', async () => {
const result = await validator.validate('list_repositories', {
visibility: 'invalid'
});
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
});
});
describe('fetch_file', () => {
it('should validate valid params', async () => {
const result = await validator.validate('fetch_file', {
owner: 'testowner',
repo: 'testrepo',
path: 'README.md',
ref: 'main'
});
expect(result.valid).toBe(true);
});
it('should require owner, repo, and path', async () => {
const result = await validator.validate('fetch_file', {});
expect(result.valid).toBe(false);
const requiredFields = ['owner', 'repo', 'path'];
requiredFields.forEach(field => {
const hasError = result.errors.some(err => err.field === field);
expect(hasError).toBe(true);
});
});
});
describe('create_commit', () => {
it('should validate valid params', async () => {
const result = await validator.validate('create_commit', {
owner: 'testowner',
repo: 'testrepo',
branch: 'feature/test',
files: [
{ path: 'test.txt', content: 'Hello World' }
],
message: 'Test commit'
});
expect(result.valid).toBe(true);
});
it('should require all required fields', async () => {
const result = await validator.validate('create_commit', {});
expect(result.valid).toBe(false);
});
it('should reject empty files array', async () => {
const result = await validator.validate('create_commit', {
owner: 'testowner',
repo: 'testrepo',
branch: 'feature/test',
files: [],
message: 'Test commit'
});
expect(result.valid).toBe(false);
});
it('should reject files exceeding max', async () => {
const result = await validator.validate('create_commit', {
owner: 'testowner',
repo: 'testrepo',
branch: 'feature/test',
files: Array(101).fill(null).map((_, i) => ({
path: `file${i}.txt`,
content: 'content'
})),
message: 'Test commit'
});
expect(result.valid).toBe(false);
});
it('should reject message exceeding max length', async () => {
const result = await validator.validate('create_commit', {
owner: 'testowner',
repo: 'testrepo',
branch: 'feature/test',
files: [{ path: 'test.txt', content: 'content' }],
message: 'A'.repeat(1001)
});
expect(result.valid).toBe(false);
});
});
describe('create_branch', () => {
it('should validate valid branch name', async () => {
const result = await validator.validate('create_branch', {
owner: 'testowner',
repo: 'testrepo',
branch: 'feature/test-branch'
});
expect(result.valid).toBe(true);
});
it('should reject invalid branch name characters', async () => {
const result = await validator.validate('create_branch', {
owner: 'testowner',
repo: 'testrepo',
branch: 'invalid@branch'
});
expect(result.valid).toBe(false);
});
it('should use default fromBranch', async () => {
const result = await validator.validate('create_branch', {
owner: 'testowner',
repo: 'testrepo',
branch: 'feature/test'
});
expect(result.valid).toBe(true);
});
});
});
describe('formatErrors', () => {
it('should format required field errors', () => {
const ajvErrors = [
{
instancePath: '/owner',
schemaPath: '#/required',
keyword: 'required',
params: { missingProperty: 'owner' }
}
];
const formatted = validator.formatErrors(ajvErrors);
expect(formatted).toHaveLength(1);
expect(formatted[0].field).toBe('owner');
expect(formatted[0].keyword).toBe('required');
});
it('should format type errors', () => {
const ajvErrors = [
{
instancePath: '/perPage',
schemaPath: '#/properties/perPage/type',
keyword: 'type',
params: { type: 'integer' }
}
];
const formatted = validator.formatErrors(ajvErrors);
expect(formatted).toHaveLength(1);
expect(formatted[0].keyword).toBe('type');
});
it('should format minimum errors', () => {
const ajvErrors = [
{
instancePath: '/perPage',
schemaPath: '#/properties/perPage/minimum',
keyword: 'minimum',
params: { limit: 1 }
}
];
const formatted = validator.formatErrors(ajvErrors);
expect(formatted).toHaveLength(1);
expect(formatted[0].keyword).toBe('minimum');
expect(formatted[0].limit).toBe(1);
});
});
describe('hasSchema', () => {
it('should return true for tools with schemas', () => {
expect(validator.hasSchema('list_repositories')).toBe(true);
expect(validator.hasSchema('create_commit')).toBe(true);
});
it('should return false for unknown tools', () => {
expect(validator.hasSchema('unknown_tool')).toBe(false);
});
});
describe('getSchema', () => {
it('should return schema for known tool', () => {
const schema = validator.getSchema('list_repositories');
expect(schema).toBeDefined();
expect(schema.type).toBe('object');
});
it('should return null for unknown tool', () => {
const schema = validator.getSchema('unknown_tool');
expect(schema).toBeNull();
});
});
describe('reloadSchemas', () => {
it('should reload schemas', async () => {
await validator.reloadSchemas();
expect(validator.schemas.size).toBeGreaterThan(0);
expect(validator.validators.size).toBeGreaterThan(0);
});
});
});