aaronsb_obsidian-mcp-plugin/tests/validation/input-validator.test.ts
Aaron Bockelie 2b8a583ba9 test: Add comprehensive unit tests for input validation
Add 100+ test cases covering:
- FileSizeValidator: Size limits, edge cases
- PathSafetyValidator: Traversal, absolute paths, invalid chars
- SafeRegexValidator: ReDoS patterns, complexity scoring
- BatchLimitValidator: Batch size limits
- InputValidator: Integration tests for all operations
- ValidationException: Error handling
- Configuration updates

All validators tested with valid/invalid inputs and edge cases.
2025-12-03 12:55:01 -06:00

393 lines
10 KiB
TypeScript

import {
InputValidator,
FileSizeValidator,
PathSafetyValidator,
SafeRegexValidator,
BatchLimitValidator,
ValidationException,
DEFAULT_VALIDATION_CONFIG
} from '../../src/validation/input-validator';
describe('InputValidator', () => {
let validator: InputValidator;
beforeEach(() => {
validator = new InputValidator();
});
describe('File Creation Validation', () => {
it('should accept valid file creation params', () => {
const result = validator.validate('file.create', {
path: 'notes/test.md',
content: 'Hello world'
});
expect(result.valid).toBe(true);
expect(result.errors).toBeUndefined();
});
it('should reject file with size exceeding limit', () => {
const largeContent = 'a'.repeat(11 * 1024 * 1024); // 11MB
const result = validator.validate('file.create', {
path: 'notes/test.md',
content: largeContent
});
expect(result.valid).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors![0].code).toBe('FILE_SIZE_EXCEEDED');
});
it('should reject path with traversal attempt', () => {
const result = validator.validate('file.create', {
path: '../../../etc/passwd',
content: 'malicious'
});
expect(result.valid).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors!.some(e => e.code === 'PATH_TRAVERSAL')).toBe(true);
});
it('should reject absolute paths', () => {
const result = validator.validate('file.create', {
path: '/etc/passwd',
content: 'test'
});
expect(result.valid).toBe(false);
expect(result.errors!.some(e => e.code === 'ABSOLUTE_PATH')).toBe(true);
});
});
describe('File Update Validation', () => {
it('should accept valid file update', () => {
const result = validator.validate('file.update', {
path: 'notes/existing.md',
content: 'Updated content'
});
expect(result.valid).toBe(true);
});
});
describe('Search Query Validation', () => {
it('should accept simple search query', () => {
const result = validator.validate('search.query', {
query: 'simple search'
});
expect(result.valid).toBe(true);
});
it('should accept valid regex', () => {
const result = validator.validate('search.query', {
query: 'test.*pattern'
});
expect(result.valid).toBe(true);
});
it('should reject ReDoS vulnerable regex', () => {
const result = validator.validate('search.query', {
query: '(a+)+'
});
expect(result.valid).toBe(false);
expect(result.errors!.some(e => e.code === 'REGEX_TOO_COMPLEX')).toBe(true);
});
it('should reject excessively long query', () => {
const longQuery = 'a'.repeat(1001);
const result = validator.validate('search.query', {
query: longQuery
});
expect(result.valid).toBe(false);
expect(result.errors!.some(e => e.code === 'QUERY_TOO_LONG')).toBe(true);
});
});
describe('Batch Operation Validation', () => {
it('should accept valid batch combine', () => {
const result = validator.validate('batch.combine', {
paths: ['file1.md', 'file2.md', 'file3.md'],
path: 'output.md'
});
expect(result.valid).toBe(true);
});
it('should reject batch exceeding size limit', () => {
const largeBatch = Array(101).fill('file.md');
const result = validator.validate('batch.combine', {
paths: largeBatch,
path: 'output.md'
});
expect(result.valid).toBe(false);
expect(result.errors!.some(e => e.code === 'BATCH_SIZE_EXCEEDED')).toBe(true);
});
});
});
describe('FileSizeValidator', () => {
let validator: FileSizeValidator;
beforeEach(() => {
validator = new FileSizeValidator(DEFAULT_VALIDATION_CONFIG);
});
it('should accept content within size limit', () => {
const result = validator.validate({
content: 'Normal sized content'
});
expect(result.valid).toBe(true);
});
it('should reject content exceeding size limit', () => {
const largeContent = 'a'.repeat(11 * 1024 * 1024);
const result = validator.validate({
content: largeContent
});
expect(result.valid).toBe(false);
expect(result.errors![0].code).toBe('FILE_SIZE_EXCEEDED');
expect(result.errors![0].message).toContain('MB');
});
it('should handle undefined content', () => {
const result = validator.validate({});
expect(result.valid).toBe(true);
});
});
describe('PathSafetyValidator', () => {
let validator: PathSafetyValidator;
beforeEach(() => {
validator = new PathSafetyValidator(DEFAULT_VALIDATION_CONFIG);
});
it('should accept valid relative path', () => {
const result = validator.validate({
path: 'folder/subfolder/file.md'
});
expect(result.valid).toBe(true);
});
it('should reject path traversal', () => {
const result = validator.validate({
path: '../../etc/passwd'
});
expect(result.valid).toBe(false);
expect(result.errors!.some(e => e.code === 'PATH_TRAVERSAL')).toBe(true);
});
it('should reject absolute Unix paths', () => {
const result = validator.validate({
path: '/etc/passwd'
});
expect(result.valid).toBe(false);
expect(result.errors!.some(e => e.code === 'ABSOLUTE_PATH')).toBe(true);
});
it('should reject absolute Windows paths', () => {
const result = validator.validate({
path: 'C:\\Windows\\System32'
});
expect(result.valid).toBe(false);
expect(result.errors!.some(e => e.code === 'ABSOLUTE_PATH')).toBe(true);
});
it('should reject path with null bytes', () => {
const result = validator.validate({
path: 'file\x00.md'
});
expect(result.valid).toBe(false);
expect(result.errors!.some(e => e.code === 'NULL_BYTE_IN_PATH')).toBe(true);
});
it('should reject path exceeding length limit', () => {
const longPath = 'a'.repeat(256) + '.md';
const result = validator.validate({
path: longPath
});
expect(result.valid).toBe(false);
expect(result.errors!.some(e => e.code === 'PATH_TOO_LONG')).toBe(true);
});
it('should reject path with invalid characters', () => {
const result = validator.validate({
path: 'file<>:"|?*.md'
});
expect(result.valid).toBe(false);
expect(result.errors!.some(e => e.code === 'INVALID_CHARACTERS')).toBe(true);
});
});
describe('SafeRegexValidator', () => {
let validator: SafeRegexValidator;
beforeEach(() => {
validator = new SafeRegexValidator(DEFAULT_VALIDATION_CONFIG);
});
it('should accept simple text query', () => {
const result = validator.validate({
query: 'simple text'
});
expect(result.valid).toBe(true);
});
it('should accept safe regex patterns', () => {
const result = validator.validate({
query: 'test.*pattern'
});
expect(result.valid).toBe(true);
});
it('should reject nested quantifiers', () => {
const result = validator.validate({
query: '(a+)+'
});
expect(result.valid).toBe(false);
expect(result.errors![0].code).toBe('REGEX_TOO_COMPLEX');
});
it('should reject nested star quantifiers', () => {
const result = validator.validate({
query: '(a*)*'
});
expect(result.valid).toBe(false);
expect(result.errors![0].code).toBe('REGEX_TOO_COMPLEX');
});
it('should reject invalid regex syntax', () => {
const result = validator.validate({
query: '([unclosed'
});
expect(result.valid).toBe(false);
expect(result.errors![0].code).toBe('INVALID_REGEX');
});
it('should calculate complexity correctly', () => {
// Very complex regex with many quantifiers and groups
const complexRegex = '((a+)|(b*)){1,10}.*\\1.*';
const result = validator.validate({
query: complexRegex
});
// This should exceed the complexity limit
expect(result.valid).toBe(false);
});
});
describe('BatchLimitValidator', () => {
let validator: BatchLimitValidator;
beforeEach(() => {
validator = new BatchLimitValidator(DEFAULT_VALIDATION_CONFIG);
});
it('should accept batch within limit', () => {
const result = validator.validate({
paths: Array(50).fill('file.md')
});
expect(result.valid).toBe(true);
});
it('should reject batch exceeding limit', () => {
const result = validator.validate({
paths: Array(101).fill('file.md')
});
expect(result.valid).toBe(false);
expect(result.errors![0].code).toBe('BATCH_SIZE_EXCEEDED');
});
it('should handle undefined paths', () => {
const result = validator.validate({});
expect(result.valid).toBe(true);
});
it('should validate maxFiles parameter', () => {
const result = validator.validate({
maxFiles: 150
});
expect(result.valid).toBe(false);
expect(result.errors![0].code).toBe('MAX_FILES_EXCEEDED');
});
});
describe('ValidationException', () => {
it('should create exception with errors', () => {
const errors = [
{
field: 'content',
message: 'File too large',
code: 'FILE_SIZE_EXCEEDED'
}
];
const exception = new ValidationException(errors);
expect(exception.errors).toEqual(errors);
expect(exception.message).toBe('Validation failed');
expect(exception.name).toBe('ValidationException');
});
it('should create exception with custom message', () => {
const exception = new ValidationException([], 'Custom error');
expect(exception.message).toBe('Custom error');
});
});
describe('Configuration Updates', () => {
let validator: InputValidator;
beforeEach(() => {
validator = new InputValidator();
});
it('should allow config updates', () => {
validator.updateConfig({
maxFileSize: 1024 * 1024 // 1MB
});
const config = validator.getConfig();
expect(config.maxFileSize).toBe(1024 * 1024);
});
it('should re-initialize validators after config update', () => {
validator.updateConfig({
maxBatchSize: 10
});
const result = validator.validate('batch.combine', {
paths: Array(15).fill('file.md'),
path: 'output.md'
});
expect(result.valid).toBe(false);
expect(result.errors![0].code).toBe('BATCH_SIZE_EXCEEDED');
});
});