mirror of
https://github.com/aaronsb/obsidian-mcp-plugin.git
synced 2026-07-22 06:45:14 +00:00
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.
393 lines
10 KiB
TypeScript
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');
|
|
});
|
|
});
|