Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
logger.test.tsโ€ข11.1 kB
/** * Tests for MCP-safe logger */ import { jest } from '@jest/globals'; import { logger } from '../../../src/utils/logger.js'; describe('MCPLogger', () => { let consoleErrorSpy: jest.SpiedFunction<typeof console.error>; let consoleWarnSpy: jest.SpiedFunction<typeof console.warn>; beforeEach(() => { // Clear logs before each test logger.clearLogs(); // Mock console methods consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); }); afterEach(() => { consoleErrorSpy.mockRestore(); consoleWarnSpy.mockRestore(); }); describe('logging methods', () => { it('should store debug messages in memory', () => { logger.debug('Test debug message', { data: 'test' }); const logs = logger.getLogs(); expect(logs).toHaveLength(1); expect(logs[0].level).toBe('debug'); expect(logs[0].message).toBe('Test debug message'); expect(logs[0].data).toEqual({ data: 'test' }); }); it('should store info messages in memory', () => { logger.info('Test info message'); const logs = logger.getLogs(); expect(logs).toHaveLength(1); expect(logs[0].level).toBe('info'); expect(logs[0].message).toBe('Test info message'); }); it('should store warn messages in memory', () => { logger.warn('Test warning', { code: 'WARN001' }); const logs = logger.getLogs(); expect(logs).toHaveLength(1); expect(logs[0].level).toBe('warn'); expect(logs[0].message).toBe('Test warning'); expect(logs[0].data).toEqual({ code: 'WARN001' }); }); it('should store error messages in memory', () => { logger.error('Test error', { error: new Error('Test') }); const logs = logger.getLogs(); expect(logs).toHaveLength(1); expect(logs[0].level).toBe('error'); expect(logs[0].message).toBe('Test error'); }); }); describe('memory management', () => { it('should maintain circular buffer of 1000 entries', () => { // Add 1005 log entries for (let i = 0; i < 1005; i++) { logger.info(`Log entry ${i}`); } const logs = logger.getLogs(1000); expect(logs).toHaveLength(1000); // First 5 entries should have been removed expect(logs[0].message).toBe('Log entry 5'); expect(logs[999].message).toBe('Log entry 1004'); }); it('should clear logs when requested', () => { logger.info('Test 1'); logger.info('Test 2'); expect(logger.getLogs()).toHaveLength(2); logger.clearLogs(); expect(logger.getLogs()).toHaveLength(0); }); }); describe('log filtering', () => { it('should filter logs by level', () => { logger.debug('Debug message'); logger.info('Info message'); logger.warn('Warning message'); logger.error('Error message'); const errorLogs = logger.getLogs(100, 'error'); expect(errorLogs).toHaveLength(1); expect(errorLogs[0].message).toBe('Error message'); const warnLogs = logger.getLogs(100, 'warn'); expect(warnLogs).toHaveLength(1); expect(warnLogs[0].message).toBe('Warning message'); }); it('should respect count limit', () => { for (let i = 0; i < 10; i++) { logger.info(`Message ${i}`); } const logs = logger.getLogs(5); expect(logs).toHaveLength(5); // Should get the last 5 messages expect(logs[0].message).toBe('Message 5'); expect(logs[4].message).toBe('Message 9'); }); }); describe('console output suppression', () => { it('should not output to console in test environment', () => { logger.error('Critical error'); logger.warn('Warning'); logger.info('Info'); logger.debug('Debug'); // In test environment, nothing should be logged to console expect(consoleErrorSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).not.toHaveBeenCalled(); }); }); describe('timestamp handling', () => { it('should add timestamps to all log entries', () => { const before = Date.now(); logger.info('Test message'); const after = Date.now(); const logs = logger.getLogs(); expect(logs[0].timestamp).toBeDefined(); expect(logs[0].timestamp instanceof Date).toBe(true); const logTime = logs[0].timestamp.getTime(); expect(logTime).toBeGreaterThanOrEqual(before); expect(logTime).toBeLessThanOrEqual(after); }); }); describe('data handling', () => { it('should handle undefined data', () => { logger.info('Message without data'); const logs = logger.getLogs(); expect(logs[0].data).toBeUndefined(); }); it('should handle complex data objects', () => { const complexData = { user: { id: 123, name: 'Test' }, message: 'Test message', // Changed from 'error' to avoid sanitization array: [1, 2, 3], nested: { deep: { value: 'test' } } }; logger.error('Complex error', complexData); const logs = logger.getLogs(); expect(logs[0].data).toEqual(complexData); }); it('should sanitize sensitive fields', () => { const sensitiveData = { user: 'testuser', password: 'secret123', api_key: 'sk-1234567890', token: 'bearer-token-here', oauth_token: 'oauth-direct-token', nested: { access_token: 'nested-token', refresh_token: 'refresh-token', safe_field: 'not-sensitive' } }; logger.info('Sensitive data test', sensitiveData); const logs = logger.getLogs(); expect(logs[0].data).toEqual({ user: 'testuser', password: '[REDACTED]', // Exact match api_key: '[REDACTED]', // Substring match token: '[REDACTED]', // Exact match oauth_token: '[REDACTED]', // Substring match (contains 'oauth') nested: { access_token: '[REDACTED]', // Substring match refresh_token: '[REDACTED]', // Substring match safe_field: 'not-sensitive' // Not sensitive, preserved } }); }); it('should not have false positives for similar field names', () => { const data = { error_message: 'This is an error', // Should NOT be redacted (no "error" in sensitive list) password_hint: 'Should be preserved', // Should be preserved (not exact match) user_key_info: 'Some info', // Should be preserved (not exact match for "key") authentication_method: 'oauth2' // Should be preserved (not exact match for "auth") }; logger.info('False positive test', data); const logs = logger.getLogs(); expect(logs[0].data).toEqual({ error_message: 'This is an error', password_hint: 'Should be preserved', user_key_info: 'Some info', authentication_method: 'oauth2' }); }); it('should handle circular references without crashing', () => { const circularData: any = { name: 'test', password: 'secret' }; circularData.self = circularData; // Create circular reference logger.info('Circular reference test', circularData); const logs = logger.getLogs(); expect(logs[0].data).toEqual({ name: 'test', password: '[REDACTED]', self: '[CIRCULAR_REFERENCE]' }); }); it('should handle deeply nested objects with depth limiting', () => { // Create a deeply nested object (15 levels deep) let deepObject: any = { value: 'bottom' }; for (let i = 0; i < 15; i++) { deepObject = { level: i, nested: deepObject }; } logger.info('Deep nesting test', { data: deepObject }); const logs = logger.getLogs(); const result = logs[0].data.data; // Count the nesting depth let depth = 0; let current = result; while (current && current.nested && depth < 20) { depth++; current = current.nested; } // Should stop at MAX_DEPTH (10) expect(depth).toBeLessThanOrEqual(10); expect(JSON.stringify(result)).toContain('[DEEP_OBJECT_TRUNCATED]'); }); it('should handle arrays with sensitive data', () => { const arrayData = { users: [ { name: 'user1', password: 'pass1' }, { name: 'user2', token: 'token2' } ], api_keys: ['key1', 'key2', 'key3'] }; logger.info('Array test', arrayData); const logs = logger.getLogs(); expect(logs[0].data).toEqual({ users: [ { name: 'user1', password: '[REDACTED]' }, { name: 'user2', token: '[REDACTED]' } ], api_keys: '[REDACTED]' // Entire field is sensitive }); }); it('should sanitize sensitive data in log messages', () => { // Test various formats of sensitive data in messages logger.error('Failed to authenticate with token: sk-1234567890abcdef'); logger.warn('API key=api_key_12345 is invalid'); logger.info('Using Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'); logger.debug('password: mysecretpass123 failed'); logger.error('client_secret=cs_987654321 expired'); const logs = logger.getLogs(); // Check that sensitive data is redacted in messages expect(logs[0].message).toBe('Failed to authenticate with token:[REDACTED]'); expect(logs[1].message).toBe('API key=[REDACTED] is invalid'); expect(logs[2].message).toBe('Using Bearer [REDACTED]'); expect(logs[3].message).toBe('password:[REDACTED] failed'); expect(logs[4].message).toBe('client_secret=[REDACTED] expired'); }); it('should preserve non-sensitive message content', () => { logger.info('This is a normal message with no secrets'); logger.error('Error: Database connection failed'); logger.warn('Warning: High memory usage detected'); const logs = logger.getLogs(); // Messages without sensitive data should remain unchanged expect(logs[0].message).toBe('This is a normal message with no secrets'); expect(logs[1].message).toBe('Error: Database connection failed'); expect(logs[2].message).toBe('Warning: High memory usage detected'); }); it('should handle API key patterns in messages', () => { logger.error('Invalid API key: sk-proj-1234567890abcdefghijklmnop'); logger.warn('Using pk-test-51234567890'); logger.info('api-key-abcdef1234567890 is deprecated'); const logs = logger.getLogs(); expect(logs[0].message).toBe('Invalid API key:[REDACTED]'); expect(logs[1].message).toBe('Using pk-[REDACTED]'); expect(logs[2].message).toBe('api[REDACTED] is deprecated'); }); }); });

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/DollhouseMCP/DollhouseMCP'

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