/** * Unit tests for utility functions, particularly validation functions */ import { validateOllamaUrl, validateModelName, validatePluginSettings, safeParseJson, isValidHttpUrl, } from '../src/utils'; describe('Validation Functions', () => { describe('validateOllamaUrl', () => { it('should accept valid HTTP URLs', () => { expect(validateOllamaUrl('http://localhost:11434').valid).toBe(true); expect(validateOllamaUrl('https://ollama.example.com').valid).toBe(true); expect(validateOllamaUrl('http://192.168.1.100:8080').valid).toBe(true); }); it('should reject invalid URLs', () => { const invalidUrl = validateOllamaUrl('not-a-url'); expect(invalidUrl.valid).toBe(false); expect(invalidUrl.error).toContain('valid HTTP or HTTPS URL'); }); it('should reject URLs ending with slash', () => { const urlWithSlash = validateOllamaUrl('http://localhost:11434/'); expect(urlWithSlash.valid).toBe(false); expect(urlWithSlash.error).toContain('should not end with a slash'); }); it('should reject empty URLs', () => { const emptyUrl = validateOllamaUrl(''); expect(emptyUrl.valid).toBe(false); expect(emptyUrl.error).toContain('cannot be empty'); }); it('should reject non-string inputs', () => { const nonString = validateOllamaUrl(123 as any); expect(nonString.valid).toBe(false); }); it('should trim whitespace from URLs', () => { expect(validateOllamaUrl(' http://localhost:11434 ').valid).toBe(true); }); }); describe('validateModelName', () => { it('should accept valid model names', () => { expect(validateModelName('llama3').valid).toBe(true); expect(validateModelName('llama-2').valid).toBe(true); expect(validateModelName('llama.2').valid).toBe(true); expect(validateModelName('llama_2').valid).toBe(true); expect(validateModelName('llama-2-7b-instruct').valid).toBe(true); }); it('should reject invalid model names', () => { const invalidModel = validateModelName('llama@2'); expect(invalidModel.valid).toBe(false); expect(invalidModel.error).toContain( 'only contain letters, numbers, dots, dashes, and underscores' ); }); it('should reject empty model names', () => { const emptyModel = validateModelName(''); expect(emptyModel.valid).toBe(false); expect(emptyModel.error).toContain('cannot be empty'); }); it('should reject model names that are too short', () => { const shortModel = validateModelName('a'); expect(shortModel.valid).toBe(false); expect(shortModel.error).toContain('at least 2 characters long'); }); it('should reject model names that are too long', () => { const longModel = validateModelName('a'.repeat(101)); expect(longModel.valid).toBe(false); expect(longModel.error).toContain('less than 100 characters long'); }); it('should reject non-string inputs', () => { const nonString = validateModelName(123 as any); expect(nonString.valid).toBe(false); }); it('should trim whitespace from model names', () => { expect(validateModelName(' llama3 ').valid).toBe(true); }); }); describe('validatePluginSettings', () => { it('should return empty array for valid settings', () => { const errors = validatePluginSettings({ ollamaUrl: 'http://localhost:11434', model: 'llama3', lastIndexTime: 0, }); expect(errors).toEqual([]); }); it('should report URL validation errors', () => { const errors = validatePluginSettings({ ollamaUrl: 'invalid-url', model: 'llama3', lastIndexTime: 0, }); expect(errors.length).toBe(1); expect(errors[0]).toContain('Ollama URL'); }); it('should report model validation errors', () => { const errors = validatePluginSettings({ ollamaUrl: 'http://localhost:11434', model: 'invalid@model', lastIndexTime: 0, }); expect(errors.length).toBe(1); expect(errors[0]).toContain('Model'); }); it('should report multiple validation errors', () => { const errors = validatePluginSettings({ ollamaUrl: 'invalid-url', model: 'invalid@model', lastIndexTime: 0, }); expect(errors.length).toBe(2); expect(errors[0]).toContain('Ollama URL'); expect(errors[1]).toContain('Model'); }); }); describe('safeParseJson', () => { it('should parse valid JSON', () => { const result = safeParseJson('{"key": "value"}'); expect(result).toEqual({ key: 'value' }); }); it('should throw on invalid JSON', () => { expect(() => safeParseJson('{invalid json}')).toThrow('Invalid JSON'); }); it('should throw on code injection patterns', () => { expect(() => safeParseJson('{"constructor": "function(){}"}')).toThrow('dangerous'); expect(() => safeParseJson('{"prototype": "something"}')).toThrow('dangerous'); expect(() => safeParseJson('{"__proto__": "something"}')).toThrow('dangerous'); expect(() => safeParseJson('{"function": "alert"}')).toThrow('dangerous'); }); it('should throw on deeply nested JSON', () => { const deeplyNested = JSON.stringify({ value: 1 }); let current = deeplyNested; for (let i = 0; i < 25; i++) { current = `{ "nested": ${current} }`; } expect(() => safeParseJson(current)).toThrow('nesting too deep'); }); it('should throw on very large JSON', () => { const largeJson = '"' + 'x'.repeat(1000001) + '"'; // 1,000,001 characters expect(() => safeParseJson(largeJson)).toThrow('input too large'); }); it('should handle string vs object arguments correctly', () => { const stringArgs = '{"path": "test.md", "content": "test"}'; const objectArgs = { path: 'test.md', content: 'test' }; const stringResult = safeParseJson(stringArgs); const objectResult = safeParseJson(JSON.stringify(objectArgs)); expect(stringResult).toEqual(objectResult); }); it('should throw on non-string input', () => { expect(() => safeParseJson(123 as any)).toThrow('Input must be a string'); }); }); describe('isValidHttpUrl', () => { it('should accept valid HTTP URLs', () => { expect(isValidHttpUrl('http://localhost:11434')).toBe(true); expect(isValidHttpUrl('https://example.com')).toBe(true); }); it('should reject non-HTTP URLs', () => { expect(isValidHttpUrl('ftp://example.com')).toBe(false); expect(isValidHttpUrl('file:///path')).toBe(false); }); it('should reject invalid URLs', () => { expect(isValidHttpUrl('not-a-url')).toBe(false); expect(isValidHttpUrl('')).toBe(false); }); }); });