Skip to main content
Glama
security.test.ts9.74 kB
import { describe, it, expect } from 'vitest'; import { sanitizePath, validateToolArgs, sanitizeString, maskSensitiveData, SecurityError, ValidationError, RateLimiter, } from '../src/utils/security.js'; import * as path from 'path'; import * as os from 'os'; describe('sanitizePath', () => { const workspaceRoot = '/workspace/project'; it('should allow paths within workspace', () => { const result = sanitizePath('src/file.al', workspaceRoot); expect(result).toBe(path.resolve(workspaceRoot, 'src/file.al')); }); it('should allow nested paths within workspace', () => { const result = sanitizePath('src/tables/MyTable.Table.al', workspaceRoot); expect(result).toBe(path.resolve(workspaceRoot, 'src/tables/MyTable.Table.al')); }); it('should reject path traversal with ../', () => { expect(() => sanitizePath('../outside.txt', workspaceRoot)).toThrow(SecurityError); }); it('should reject path traversal with multiple ../', () => { expect(() => sanitizePath('src/../../outside.txt', workspaceRoot)).toThrow(SecurityError); }); it('should reject absolute paths outside workspace', () => { expect(() => sanitizePath('/etc/passwd', workspaceRoot)).toThrow(SecurityError); }); it('should reject URL-encoded path traversal', () => { expect(() => sanitizePath('%2e%2e/outside.txt', workspaceRoot)).toThrow(SecurityError); }); it('should reject null bytes', () => { expect(() => sanitizePath('file\0.txt', workspaceRoot)).toThrow(SecurityError); }); }); describe('validateToolArgs', () => { it('should pass with all required arguments', () => { const args = { path: '/some/path', content: 'hello' }; expect(() => validateToolArgs(args, ['path', 'content'], {})).not.toThrow(); }); it('should throw for missing required argument', () => { const args = { path: '/some/path' }; expect(() => validateToolArgs(args, ['path', 'content'], {})).toThrow(ValidationError); }); it('should validate types correctly', () => { const args = { count: 5, name: 'test' }; expect(() => validateToolArgs(args, [], { count: 'number', name: 'string' })).not.toThrow(); }); it('should throw for incorrect type', () => { const args = { count: '5' }; // string instead of number expect(() => validateToolArgs(args, [], { count: 'number' })).toThrow(ValidationError); }); }); describe('sanitizeString', () => { it('should return valid strings unchanged', () => { expect(sanitizeString('hello world')).toBe('hello world'); }); it('should remove null bytes', () => { expect(sanitizeString('hello\0world')).toBe('helloworld'); }); it('should truncate strings exceeding maxLength', () => { const longString = 'a'.repeat(100); expect(sanitizeString(longString, 50)).toBe('a'.repeat(50)); }); it('should throw for non-string input', () => { expect(() => sanitizeString(123 as unknown as string)).toThrow(ValidationError); }); }); describe('maskSensitiveData', () => { it('should mask API keys in objects', () => { const obj = { apiKey: 'super-secret-api-key-12345' }; const result = maskSensitiveData(obj) as Record<string, unknown>; expect(result.apiKey).toBe('[REDACTED]'); }); it('should mask passwords in objects', () => { const obj = { password: 'mypassword123' }; const result = maskSensitiveData(obj) as Record<string, unknown>; expect(result.password).toBe('[REDACTED]'); }); it('should mask authorization headers', () => { const obj = { authorization: 'Bearer token12345678901234567890' }; const result = maskSensitiveData(obj) as Record<string, unknown>; expect(result.authorization).toBe('[REDACTED]'); }); it('should mask long strings that look like tokens', () => { const token = 'abcdefghijklmnopqrstuvwxyz1234567890'; const result = maskSensitiveData(token) as string; expect(result).toContain('[MASKED:'); expect(result).toContain('...'); expect(result).toContain(']'); }); it('should not mask short normal strings', () => { const obj = { name: 'John' }; const result = maskSensitiveData(obj) as Record<string, unknown>; expect(result.name).toBe('John'); }); it('should handle nested objects', () => { const obj = { user: { name: 'John', api_key: 'secret123456789012345' } }; const result = maskSensitiveData(obj) as Record<string, Record<string, unknown>>; expect(result.user.name).toBe('John'); expect(result.user.api_key).toBe('[REDACTED]'); }); it('should handle arrays', () => { const arr = [{ apiKey: 'secret' }, { name: 'test' }]; const result = maskSensitiveData(arr) as Array<Record<string, unknown>>; expect(result[0].apiKey).toBe('[REDACTED]'); expect(result[1].name).toBe('test'); }); }); describe('RateLimiter', () => { it('should allow requests within limit', () => { const limiter = new RateLimiter(5, 1000); expect(limiter.isAllowed()).toBe(true); expect(limiter.isAllowed()).toBe(true); expect(limiter.isAllowed()).toBe(true); expect(limiter.remaining()).toBe(2); }); it('should block requests exceeding limit', () => { const limiter = new RateLimiter(3, 1000); expect(limiter.isAllowed()).toBe(true); expect(limiter.isAllowed()).toBe(true); expect(limiter.isAllowed()).toBe(true); expect(limiter.isAllowed()).toBe(false); expect(limiter.remaining()).toBe(0); }); it('should reset after window expires', async () => { const limiter = new RateLimiter(2, 100); // 100ms window expect(limiter.isAllowed()).toBe(true); expect(limiter.isAllowed()).toBe(true); expect(limiter.isAllowed()).toBe(false); // Wait for window to reset await new Promise(resolve => setTimeout(resolve, 150)); expect(limiter.isAllowed()).toBe(true); }); it('should return 0 resetIn when no requests', () => { const limiter = new RateLimiter(5, 1000); expect(limiter.resetIn()).toBe(0); }); it('should return time until reset when requests made', () => { const limiter = new RateLimiter(5, 1000); limiter.isAllowed(); const resetIn = limiter.resetIn(); expect(resetIn).toBeGreaterThan(0); expect(resetIn).toBeLessThanOrEqual(1000); }); }); describe('sanitizePath edge cases', () => { const workspaceRoot = '/workspace/project'; it('should handle double URL-encoded path traversal', () => { expect(() => sanitizePath('%252e%252e/outside.txt', workspaceRoot)).toThrow(SecurityError); }); it('should handle backslash path separators', () => { // On Windows-like paths const result = sanitizePath('src\\tables\\MyTable.al', workspaceRoot); expect(result).toContain('MyTable.al'); }); }); describe('maskSensitiveData edge cases', () => { it('should handle null values', () => { expect(maskSensitiveData(null)).toBeNull(); }); it('should handle undefined values', () => { expect(maskSensitiveData(undefined)).toBeUndefined(); }); it('should handle primitive values', () => { expect(maskSensitiveData(42)).toBe(42); expect(maskSensitiveData(true)).toBe(true); }); it('should handle empty objects', () => { const result = maskSensitiveData({}); expect(result).toEqual({}); }); it('should handle empty arrays', () => { const result = maskSensitiveData([]); expect(result).toEqual([]); }); it('should mask secrets in objects', () => { const obj = { secret: 'mysecretvalue123' }; const result = maskSensitiveData(obj) as Record<string, unknown>; expect(result.secret).toBe('[REDACTED]'); }); it('should mask tokens in objects', () => { const obj = { token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' }; const result = maskSensitiveData(obj) as Record<string, unknown>; expect(result.token).toBe('[REDACTED]'); }); it('should mask credentials in objects', () => { const obj = { credential: 'user:pass' }; const result = maskSensitiveData(obj) as Record<string, unknown>; expect(result.credential).toBe('[REDACTED]'); }); it('should not mask empty sensitive fields', () => { const obj = { apiKey: '' }; const result = maskSensitiveData(obj) as Record<string, unknown>; expect(result.apiKey).toBe(''); }); it('should handle very deep nesting', () => { const deepObj = { a: { b: { c: { d: { e: { f: { g: { h: { i: { j: { k: 'value' } } } } } } } } } } }; const result = maskSensitiveData(deepObj); expect(result).toBeDefined(); }); }); describe('validateToolArgs edge cases', () => { it('should handle null values as missing', () => { const args = { path: null }; expect(() => validateToolArgs(args as Record<string, unknown>, ['path'], {})).toThrow(ValidationError); }); it('should handle undefined values as missing', () => { const args = { path: undefined }; expect(() => validateToolArgs(args as Record<string, unknown>, ['path'], {})).toThrow(ValidationError); }); it('should validate array type', () => { const args = { items: [1, 2, 3] }; expect(() => validateToolArgs(args, [], { items: 'array' })).not.toThrow(); }); it('should reject non-array when array expected', () => { const args = { items: 'not an array' }; expect(() => validateToolArgs(args, [], { items: 'array' })).toThrow(ValidationError); }); it('should validate object type', () => { const args = { config: { key: 'value' } }; expect(() => validateToolArgs(args, [], { config: 'object' })).not.toThrow(); }); it('should validate boolean type', () => { const args = { enabled: false }; expect(() => validateToolArgs(args, [], { enabled: 'boolean' })).not.toThrow(); }); });

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/ciellosinc/partnercore-proxy'

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