75fa07f148
Remove unused isValidHttpUrl function and related tests Remove commented-out model validation regex from constants ```
301 lines
11 KiB
TypeScript
Executable File
301 lines
11 KiB
TypeScript
Executable File
/**
|
|
* Unit tests for utility functions, particularly validation functions
|
|
*/
|
|
import { LogLevel, Logger } from '../src/utils';
|
|
import {
|
|
validateOllamaUrl,
|
|
validateModelName,
|
|
validatePluginSettings,
|
|
safeParseJson,
|
|
} 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, underscores, and colons'
|
|
);
|
|
});
|
|
|
|
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',
|
|
});
|
|
expect(errors).toEqual([]);
|
|
});
|
|
|
|
it('should report URL validation errors', () => {
|
|
const errors = validatePluginSettings({
|
|
ollamaUrl: 'invalid-url',
|
|
model: 'llama3',
|
|
});
|
|
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',
|
|
});
|
|
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',
|
|
});
|
|
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 dangerous object keys', () => {
|
|
expect(() => safeParseJson('{"constructor": "function(){}"}')).toThrow('dangerous');
|
|
expect(() => safeParseJson('{"prototype": "something"}')).toThrow('dangerous');
|
|
expect(() => safeParseJson('{"__proto__": "something"}')).toThrow('dangerous');
|
|
});
|
|
|
|
it('should allow legitimate content containing function-related words', () => {
|
|
// Should not throw - these are legitimate values, not dangerous keys
|
|
expect(() =>
|
|
safeParseJson('{"message": "The function constructor is used to create objects"}')
|
|
).not.toThrow();
|
|
expect(() =>
|
|
safeParseJson('{"content": "JavaScript prototype inheritance is powerful"}')
|
|
).not.toThrow();
|
|
expect(() => safeParseJson('{"code": "function example() { return true; }"}')).not.toThrow();
|
|
expect(() => safeParseJson('{"function": "alert"}')).not.toThrow(); // function as key is now allowed
|
|
});
|
|
|
|
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('Logger', () => {
|
|
beforeEach(() => {
|
|
// Reset logger level before each test
|
|
Logger.setLevel(LogLevel.DEBUG);
|
|
});
|
|
|
|
describe('setLevel', () => {
|
|
it('should set level using string values', () => {
|
|
Logger.setLevel('debug');
|
|
expect((Logger as any).minLevel).toBe(LogLevel.DEBUG);
|
|
|
|
Logger.setLevel('info');
|
|
expect((Logger as any).minLevel).toBe(LogLevel.INFO);
|
|
|
|
Logger.setLevel('warn');
|
|
expect((Logger as any).minLevel).toBe(LogLevel.WARN);
|
|
|
|
Logger.setLevel('error');
|
|
expect((Logger as any).minLevel).toBe(LogLevel.ERROR);
|
|
});
|
|
|
|
it('should set level using numeric values', () => {
|
|
Logger.setLevel(LogLevel.DEBUG);
|
|
expect((Logger as any).minLevel).toBe(LogLevel.DEBUG);
|
|
|
|
Logger.setLevel(LogLevel.INFO);
|
|
expect((Logger as any).minLevel).toBe(LogLevel.INFO);
|
|
|
|
Logger.setLevel(LogLevel.WARN);
|
|
expect((Logger as any).minLevel).toBe(LogLevel.WARN);
|
|
|
|
Logger.setLevel(LogLevel.ERROR);
|
|
expect((Logger as any).minLevel).toBe(LogLevel.ERROR);
|
|
});
|
|
|
|
it('should default to DEBUG for unknown string levels', () => {
|
|
Logger.setLevel('unknown-level');
|
|
expect((Logger as any).minLevel).toBe(LogLevel.DEBUG);
|
|
});
|
|
|
|
it('should be case insensitive for string levels', () => {
|
|
Logger.setLevel('DEBUG');
|
|
expect((Logger as any).minLevel).toBe(LogLevel.DEBUG);
|
|
|
|
Logger.setLevel('Info');
|
|
expect((Logger as any).minLevel).toBe(LogLevel.INFO);
|
|
});
|
|
});
|
|
|
|
describe('logging methods with different levels', () => {
|
|
it('should log debug messages when level is DEBUG', () => {
|
|
const consoleSpy = jest.spyOn(console, 'debug').mockImplementation();
|
|
Logger.setLevel('debug');
|
|
|
|
Logger.debug('test message', 'test-category');
|
|
Logger.info('test message', 'test-category');
|
|
Logger.warn('test message', 'test-category');
|
|
Logger.error('test message', 'test-category');
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith('[test-category] DEBUG: test message');
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('should not log debug messages when level is INFO', () => {
|
|
const consoleSpy = jest.spyOn(console, 'debug').mockImplementation();
|
|
Logger.setLevel('info');
|
|
|
|
Logger.debug('test message', 'test-category');
|
|
Logger.info('test message', 'test-category');
|
|
|
|
expect(consoleSpy).not.toHaveBeenCalled();
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('should not log debug or info messages when level is WARN', () => {
|
|
const consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation();
|
|
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
|
|
Logger.setLevel('warn');
|
|
|
|
Logger.debug('test message', 'test-category');
|
|
Logger.info('test message', 'test-category');
|
|
Logger.warn('test message', 'test-category');
|
|
|
|
expect(consoleDebugSpy).not.toHaveBeenCalled();
|
|
expect(consoleInfoSpy).not.toHaveBeenCalled();
|
|
consoleDebugSpy.mockRestore();
|
|
consoleInfoSpy.mockRestore();
|
|
});
|
|
|
|
it('should only log error messages when level is ERROR', () => {
|
|
const consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation();
|
|
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
|
|
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
Logger.setLevel('error');
|
|
|
|
Logger.debug('test message', 'test-category');
|
|
Logger.info('test message', 'test-category');
|
|
Logger.warn('test message', 'test-category');
|
|
Logger.error('test message', 'test-category');
|
|
|
|
expect(consoleDebugSpy).not.toHaveBeenCalled();
|
|
expect(consoleInfoSpy).not.toHaveBeenCalled();
|
|
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith('[test-category] ERROR: test message');
|
|
consoleDebugSpy.mockRestore();
|
|
consoleInfoSpy.mockRestore();
|
|
consoleWarnSpy.mockRestore();
|
|
consoleErrorSpy.mockRestore();
|
|
});
|
|
});
|
|
});
|