Files
mcpctl/src/mcpd/tests/resource-rules.test.ts

89 lines
3.5 KiB
TypeScript
Raw Normal View History

import { describe, it, expect } from 'vitest';
import { ResourceRuleRegistry } from '../src/validation/resource-rules.js';
import type { ResourceRule, RuleMeta, RuleContext, ValidationResult } from '../src/validation/resource-rules.js';
const noopCtx: RuleContext = {
findResource: async () => null,
};
function makeRule(
name: string,
resource: string,
matchFn: (data: unknown, meta: RuleMeta) => boolean,
validateFn: () => Promise<ValidationResult>,
): ResourceRule {
return { name, resource, match: matchFn, validate: validateFn };
}
describe('ResourceRuleRegistry', () => {
it('returns valid when no rules are registered', async () => {
const registry = new ResourceRuleRegistry();
const result = await registry.validate('prompt', { name: 'x' }, { operation: 'create' }, noopCtx);
expect(result.valid).toBe(true);
});
it('returns valid when no rules match the resource type', async () => {
const registry = new ResourceRuleRegistry();
registry.register(makeRule('server-rule', 'server', () => true, async () => ({ valid: false, errors: ['fail'] })));
const result = await registry.validate('prompt', { name: 'x' }, { operation: 'create' }, noopCtx);
expect(result.valid).toBe(true);
});
it('skips rules that do not match the predicate', async () => {
const registry = new ResourceRuleRegistry();
registry.register(makeRule('skip-rule', 'prompt', () => false, async () => ({ valid: false, errors: ['fail'] })));
const result = await registry.validate('prompt', { name: 'x' }, { operation: 'create' }, noopCtx);
expect(result.valid).toBe(true);
});
it('runs matching rules and returns errors', async () => {
const registry = new ResourceRuleRegistry();
registry.register(makeRule('bad-rule', 'prompt', () => true, async () => ({
valid: false,
errors: ['missing template var'],
})));
const result = await registry.validate('prompt', { name: 'x' }, { operation: 'update' }, noopCtx);
expect(result.valid).toBe(false);
expect(result.errors).toEqual(['missing template var']);
});
it('aggregates errors from multiple matching rules', async () => {
const registry = new ResourceRuleRegistry();
registry.register(makeRule('rule-1', 'prompt', () => true, async () => ({
valid: false,
errors: ['error A'],
})));
registry.register(makeRule('rule-2', 'prompt', () => true, async () => ({
valid: false,
errors: ['error B', 'error C'],
})));
const result = await registry.validate('prompt', { name: 'x' }, { operation: 'create' }, noopCtx);
expect(result.valid).toBe(false);
expect(result.errors).toEqual(['error A', 'error B', 'error C']);
});
it('returns valid when all matching rules pass', async () => {
const registry = new ResourceRuleRegistry();
registry.register(makeRule('ok-rule', 'prompt', () => true, async () => ({ valid: true })));
const result = await registry.validate('prompt', { name: 'x' }, { operation: 'update' }, noopCtx);
expect(result.valid).toBe(true);
});
it('passes meta to match function', async () => {
const registry = new ResourceRuleRegistry();
let receivedMeta: RuleMeta | null = null;
registry.register(makeRule('meta-check', 'prompt', (_data, meta) => {
receivedMeta = meta;
return false;
}, async () => ({ valid: true })));
await registry.validate('prompt', { name: 'x' }, { operation: 'update', isSystemResource: true, projectName: 'sys' }, noopCtx);
expect(receivedMeta).toEqual({ operation: 'update', isSystemResource: true, projectName: 'sys' });
});
});