Skip to main content
Glama
validation.test.ts8.58 kB
/** * Tests for validation utilities */ import { describe, it, expect } from 'vitest'; import { truncateString, validateString, validateStringArray, sanitizeFilename, sanitizePathComponent, validatePathWithinBase, escapeRegex, createSafeRegex, createWordMatchRegex, isValidISODate, isPositiveInteger, clampNumber, MAX_LENGTHS, } from '../validation.js'; describe('truncateString', () => { it('returns undefined for undefined input', () => { expect(truncateString(undefined, 100)).toBeUndefined(); }); it('returns string unchanged if shorter than max', () => { expect(truncateString('hello', 10)).toBe('hello'); }); it('truncates string to max length', () => { expect(truncateString('hello world', 5)).toBe('hello'); }); it('handles exact length', () => { expect(truncateString('hello', 5)).toBe('hello'); }); it('handles empty string', () => { expect(truncateString('', 10)).toBe(''); }); }); describe('validateString', () => { it('returns undefined for undefined non-required', () => { expect(validateString(undefined, 'test', 100)).toBeUndefined(); }); it('throws for undefined required field', () => { expect(() => validateString(undefined, 'test', 100, true)).toThrow('test is required'); }); it('truncates long strings', () => { expect(validateString('hello world', 'test', 5)).toBe('hello'); }); it('returns valid string unchanged', () => { expect(validateString('hello', 'test', 100)).toBe('hello'); }); }); describe('validateStringArray', () => { it('returns undefined for undefined input', () => { expect(validateStringArray(undefined, 50)).toBeUndefined(); }); it('filters and truncates array items', () => { const result = validateStringArray(['short', 'a very long string that exceeds limit'], 10); expect(result).toEqual(['short', 'a very lon']); }); it('filters empty strings', () => { const result = validateStringArray(['hello', '', 'world'], 50); expect(result).toEqual(['hello', 'world']); }); it('limits array size', () => { const largeArray = Array(200).fill('item'); const result = validateStringArray(largeArray, 50, 10); expect(result).toHaveLength(10); }); }); describe('sanitizeFilename', () => { it('replaces dangerous characters with underscore', () => { const result = sanitizeFilename('file<name'); expect(result).toContain('_'); expect(result).not.toContain('<'); }); it('replaces path separators', () => { const result = sanitizeFilename('path/file\\name'); expect(result).not.toContain('/'); expect(result).not.toContain('\\'); }); it('returns unnamed for empty string', () => { expect(sanitizeFilename('')).toBe('unnamed'); }); it('truncates long filenames to 255 chars', () => { const longName = 'a'.repeat(300); expect(sanitizeFilename(longName).length).toBeLessThanOrEqual(255); }); it('handles normal filenames', () => { expect(sanitizeFilename('my-file_2024.txt')).toBe('my-file_2024.txt'); }); it('replaces leading dots', () => { const result = sanitizeFilename('..hidden'); expect(result[0]).not.toBe('.'); }); }); describe('sanitizePathComponent', () => { it('sanitizes path traversal components', () => { // Returns underscore for pure ".." expect(sanitizePathComponent('..')).toBe('_'); }); it('replaces slashes with underscore', () => { const result = sanitizePathComponent('path/to/file'); expect(result).not.toContain('/'); }); it('returns unnamed for empty input', () => { expect(sanitizePathComponent('')).toBe('unnamed'); }); }); describe('validatePathWithinBase', () => { it('returns resolved path for path within base', () => { const result = validatePathWithinBase('/home/user/notes/file.md', '/home/user/notes'); expect(result).toBe('/home/user/notes/file.md'); }); it('returns resolved path for nested path within base', () => { const result = validatePathWithinBase('/home/user/notes/project/file.md', '/home/user/notes'); expect(result).toBe('/home/user/notes/project/file.md'); }); it('throws for path outside base', () => { expect(() => validatePathWithinBase('/etc/passwd', '/home/user/notes')).toThrow('Path traversal'); }); it('throws for path traversal attempt', () => { expect(() => validatePathWithinBase('/home/user/notes/../../../etc/passwd', '/home/user/notes')).toThrow('Path traversal'); }); it('returns base path for base path itself', () => { const result = validatePathWithinBase('/home/user/notes', '/home/user/notes'); expect(result).toBe('/home/user/notes'); }); }); describe('escapeRegex', () => { it('escapes special characters', () => { expect(escapeRegex('hello.*world')).toBe('hello\\.\\*world'); }); it('escapes all special regex chars', () => { expect(escapeRegex('[test]')).toBe('\\[test\\]'); expect(escapeRegex('a+b?c')).toBe('a\\+b\\?c'); expect(escapeRegex('(foo)')).toBe('\\(foo\\)'); }); it('leaves normal text unchanged', () => { expect(escapeRegex('hello world')).toBe('hello world'); }); }); describe('createSafeRegex', () => { it('creates regex for safe pattern', () => { const regex = createSafeRegex('hello'); expect(regex).toBeInstanceOf(RegExp); expect(regex?.test('hello world')).toBe(true); }); it('returns null for overly long pattern', () => { const longPattern = 'a'.repeat(101); expect(createSafeRegex(longPattern)).toBeNull(); }); it('escapes special characters in pattern', () => { const regex = createSafeRegex('hello.*world'); expect(regex?.test('hello.*world')).toBe(true); expect(regex?.test('helloXXXworld')).toBe(false); }); }); describe('createWordMatchRegex', () => { it('creates case-insensitive word match regex', () => { const regex = createWordMatchRegex('hello'); expect(regex).not.toBeNull(); expect(regex!.test('Hello World')).toBe(true); }); it('handles special characters', () => { const regex = createWordMatchRegex('c++'); expect(regex).not.toBeNull(); expect(regex!.test('I use c++ for development')).toBe(true); }); it('returns null for empty word', () => { expect(createWordMatchRegex('')).toBeNull(); }); it('returns null for overly long word', () => { expect(createWordMatchRegex('a'.repeat(101))).toBeNull(); }); }); describe('isValidISODate', () => { it('returns true for date-only format (YYYY-MM-DD)', () => { expect(isValidISODate('2024-01-15')).toBe(true); }); it('returns true for full ISO timestamp', () => { expect(isValidISODate('2024-01-15T10:30:00Z')).toBe(true); expect(isValidISODate('2024-01-15T10:30:00.000Z')).toBe(true); }); it('returns false for invalid date', () => { expect(isValidISODate('not-a-date')).toBe(false); }); it('returns false for empty string', () => { expect(isValidISODate('')).toBe(false); }); it('returns false for undefined', () => { expect(isValidISODate(undefined)).toBe(false); }); it('returns false for invalid date values', () => { expect(isValidISODate('2024-13-45')).toBe(false); }); }); describe('isPositiveInteger', () => { it('returns true for positive integers', () => { expect(isPositiveInteger(1)).toBe(true); expect(isPositiveInteger(100)).toBe(true); }); it('returns false for zero', () => { expect(isPositiveInteger(0)).toBe(false); }); it('returns false for negative numbers', () => { expect(isPositiveInteger(-1)).toBe(false); }); it('returns false for decimals', () => { expect(isPositiveInteger(1.5)).toBe(false); }); it('returns false for NaN', () => { expect(isPositiveInteger(NaN)).toBe(false); }); }); describe('clampNumber', () => { it('returns value within range', () => { expect(clampNumber(5, 1, 10, 5)).toBe(5); }); it('clamps to min', () => { expect(clampNumber(0, 1, 10, 5)).toBe(1); }); it('clamps to max', () => { expect(clampNumber(20, 1, 10, 5)).toBe(10); }); it('returns default for undefined', () => { expect(clampNumber(undefined, 1, 10, 5)).toBe(5); }); }); describe('MAX_LENGTHS', () => { it('has required fields', () => { expect(MAX_LENGTHS.summary).toBeDefined(); expect(MAX_LENGTHS.query).toBeDefined(); expect(MAX_LENGTHS.path).toBeDefined(); expect(MAX_LENGTHS.tag).toBeDefined(); }); it('has reasonable values', () => { expect(MAX_LENGTHS.summary).toBeGreaterThan(100); expect(MAX_LENGTHS.tag).toBeGreaterThan(10); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/VoCoufi/second-brain-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server