- Add Jest configuration for TypeScript testing - Add security utilities tests (44 tests) - Add Zod validation tests (34 tests) - Add cursor pagination tests (25 tests) - Add query builder tests (38 tests) - Add tools structure validation (68 tests) - All 164 tools validated for correct structure - Version bump to 1.3.4 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
267 lines
9.1 KiB
TypeScript
267 lines
9.1 KiB
TypeScript
/**
|
|
* Validation Utilities Tests
|
|
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
import {
|
|
schemas,
|
|
validateInput,
|
|
safeValidateInput,
|
|
formatZodError,
|
|
toolSchemas,
|
|
validateUUIDs,
|
|
validateEnum,
|
|
validateStringLength,
|
|
validateNumberRange
|
|
} from '../validation';
|
|
|
|
describe('Validation Utilities', () => {
|
|
describe('schemas', () => {
|
|
describe('uuid', () => {
|
|
it('should accept valid UUIDs', () => {
|
|
expect(schemas.uuid.safeParse('550e8400-e29b-41d4-a716-446655440000').success).toBe(true);
|
|
});
|
|
|
|
it('should reject invalid UUIDs', () => {
|
|
expect(schemas.uuid.safeParse('not-a-uuid').success).toBe(false);
|
|
expect(schemas.uuid.safeParse('').success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('email', () => {
|
|
it('should accept valid emails', () => {
|
|
expect(schemas.email.safeParse('test@example.com').success).toBe(true);
|
|
});
|
|
|
|
it('should reject invalid emails', () => {
|
|
expect(schemas.email.safeParse('notanemail').success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('pagination', () => {
|
|
it('should use defaults', () => {
|
|
const result = schemas.pagination.parse({});
|
|
expect(result.limit).toBe(25);
|
|
expect(result.offset).toBe(0);
|
|
});
|
|
|
|
it('should accept valid values', () => {
|
|
const result = schemas.pagination.parse({ limit: 50, offset: 10 });
|
|
expect(result.limit).toBe(50);
|
|
expect(result.offset).toBe(10);
|
|
});
|
|
|
|
it('should reject out of range values', () => {
|
|
expect(schemas.pagination.safeParse({ limit: 0 }).success).toBe(false);
|
|
expect(schemas.pagination.safeParse({ limit: 101 }).success).toBe(false);
|
|
expect(schemas.pagination.safeParse({ offset: -1 }).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('permission', () => {
|
|
it('should accept valid permissions', () => {
|
|
expect(schemas.permission.safeParse('read').success).toBe(true);
|
|
expect(schemas.permission.safeParse('read_write').success).toBe(true);
|
|
expect(schemas.permission.safeParse('admin').success).toBe(true);
|
|
});
|
|
|
|
it('should reject invalid permissions', () => {
|
|
expect(schemas.permission.safeParse('invalid').success).toBe(false);
|
|
expect(schemas.permission.safeParse('ADMIN').success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('userRole', () => {
|
|
it('should accept valid roles', () => {
|
|
expect(schemas.userRole.safeParse('admin').success).toBe(true);
|
|
expect(schemas.userRole.safeParse('member').success).toBe(true);
|
|
expect(schemas.userRole.safeParse('viewer').success).toBe(true);
|
|
expect(schemas.userRole.safeParse('guest').success).toBe(true);
|
|
});
|
|
|
|
it('should reject invalid roles', () => {
|
|
expect(schemas.userRole.safeParse('superadmin').success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('booleanString', () => {
|
|
it('should accept boolean values', () => {
|
|
expect(schemas.booleanString.parse(true)).toBe(true);
|
|
expect(schemas.booleanString.parse(false)).toBe(false);
|
|
});
|
|
|
|
it('should transform string values', () => {
|
|
expect(schemas.booleanString.parse('true')).toBe(true);
|
|
expect(schemas.booleanString.parse('1')).toBe(true);
|
|
expect(schemas.booleanString.parse('false')).toBe(false);
|
|
expect(schemas.booleanString.parse('0')).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('validateInput', () => {
|
|
const testSchema = z.object({
|
|
name: z.string().min(1),
|
|
age: z.number().int().positive()
|
|
});
|
|
|
|
it('should return validated data for valid input', () => {
|
|
const result = validateInput(testSchema, { name: 'John', age: 30 });
|
|
expect(result).toEqual({ name: 'John', age: 30 });
|
|
});
|
|
|
|
it('should throw ZodError for invalid input', () => {
|
|
expect(() => validateInput(testSchema, { name: '', age: -1 })).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('safeValidateInput', () => {
|
|
const testSchema = z.object({
|
|
id: z.string().uuid()
|
|
});
|
|
|
|
it('should return success for valid input', () => {
|
|
const result = safeValidateInput(testSchema, { id: '550e8400-e29b-41d4-a716-446655440000' });
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.id).toBe('550e8400-e29b-41d4-a716-446655440000');
|
|
}
|
|
});
|
|
|
|
it('should return error for invalid input', () => {
|
|
const result = safeValidateInput(testSchema, { id: 'invalid' });
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error).toBeInstanceOf(z.ZodError);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('formatZodError', () => {
|
|
it('should format errors with path', () => {
|
|
const schema = z.object({
|
|
user: z.object({
|
|
email: z.string().email()
|
|
})
|
|
});
|
|
|
|
const result = schema.safeParse({ user: { email: 'invalid' } });
|
|
if (!result.success) {
|
|
const formatted = formatZodError(result.error);
|
|
expect(formatted).toContain('user.email');
|
|
}
|
|
});
|
|
|
|
it('should format errors without path', () => {
|
|
const schema = z.string().min(5);
|
|
const result = schema.safeParse('abc');
|
|
if (!result.success) {
|
|
const formatted = formatZodError(result.error);
|
|
expect(formatted).not.toContain('.');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('toolSchemas', () => {
|
|
describe('listArgs', () => {
|
|
it('should accept valid pagination', () => {
|
|
expect(toolSchemas.listArgs.safeParse({ limit: 50, offset: 10 }).success).toBe(true);
|
|
expect(toolSchemas.listArgs.safeParse({}).success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getByIdArgs', () => {
|
|
it('should require valid UUID id', () => {
|
|
expect(toolSchemas.getByIdArgs.safeParse({ id: '550e8400-e29b-41d4-a716-446655440000' }).success).toBe(true);
|
|
expect(toolSchemas.getByIdArgs.safeParse({ id: 'invalid' }).success).toBe(false);
|
|
expect(toolSchemas.getByIdArgs.safeParse({}).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('bulkDocumentArgs', () => {
|
|
it('should require at least one document_id', () => {
|
|
expect(toolSchemas.bulkDocumentArgs.safeParse({ document_ids: [] }).success).toBe(false);
|
|
expect(toolSchemas.bulkDocumentArgs.safeParse({
|
|
document_ids: ['550e8400-e29b-41d4-a716-446655440000']
|
|
}).success).toBe(true);
|
|
});
|
|
|
|
it('should validate all UUIDs', () => {
|
|
expect(toolSchemas.bulkDocumentArgs.safeParse({
|
|
document_ids: ['550e8400-e29b-41d4-a716-446655440000', 'invalid']
|
|
}).success).toBe(false);
|
|
});
|
|
|
|
it('should limit to 100 documents', () => {
|
|
const tooMany = Array(101).fill('550e8400-e29b-41d4-a716-446655440000');
|
|
expect(toolSchemas.bulkDocumentArgs.safeParse({ document_ids: tooMany }).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('searchArgs', () => {
|
|
it('should require non-empty query', () => {
|
|
expect(toolSchemas.searchArgs.safeParse({ query: '' }).success).toBe(false);
|
|
expect(toolSchemas.searchArgs.safeParse({ query: 'test' }).success).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('validateUUIDs', () => {
|
|
it('should accept valid UUID arrays', () => {
|
|
expect(() => validateUUIDs([
|
|
'550e8400-e29b-41d4-a716-446655440000',
|
|
'6ba7b810-9dad-11d1-80b4-00c04fd430c8'
|
|
])).not.toThrow();
|
|
});
|
|
|
|
it('should throw for invalid UUIDs', () => {
|
|
expect(() => validateUUIDs(['invalid'])).toThrow();
|
|
});
|
|
|
|
it('should include field name in error', () => {
|
|
expect(() => validateUUIDs(['invalid'], 'document_ids')).toThrow('document_ids');
|
|
});
|
|
});
|
|
|
|
describe('validateEnum', () => {
|
|
const allowed = ['a', 'b', 'c'] as const;
|
|
|
|
it('should accept valid values', () => {
|
|
expect(validateEnum('a', allowed, 'test')).toBe('a');
|
|
expect(validateEnum('b', allowed, 'test')).toBe('b');
|
|
});
|
|
|
|
it('should throw for invalid values', () => {
|
|
expect(() => validateEnum('d', allowed, 'test')).toThrow('Invalid test');
|
|
expect(() => validateEnum('d', allowed, 'test')).toThrow('Allowed values');
|
|
});
|
|
});
|
|
|
|
describe('validateStringLength', () => {
|
|
it('should accept strings within range', () => {
|
|
expect(() => validateStringLength('hello', 1, 10, 'name')).not.toThrow();
|
|
expect(() => validateStringLength('a', 1, 10, 'name')).not.toThrow();
|
|
expect(() => validateStringLength('1234567890', 1, 10, 'name')).not.toThrow();
|
|
});
|
|
|
|
it('should throw for strings outside range', () => {
|
|
expect(() => validateStringLength('', 1, 10, 'name')).toThrow();
|
|
expect(() => validateStringLength('12345678901', 1, 10, 'name')).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('validateNumberRange', () => {
|
|
it('should accept numbers within range', () => {
|
|
expect(() => validateNumberRange(5, 1, 10, 'age')).not.toThrow();
|
|
expect(() => validateNumberRange(1, 1, 10, 'age')).not.toThrow();
|
|
expect(() => validateNumberRange(10, 1, 10, 'age')).not.toThrow();
|
|
});
|
|
|
|
it('should throw for numbers outside range', () => {
|
|
expect(() => validateNumberRange(0, 1, 10, 'age')).toThrow();
|
|
expect(() => validateNumberRange(11, 1, 10, 'age')).toThrow();
|
|
});
|
|
});
|
|
});
|