Skip to main content
Glama
logger.test.js26.4 kB
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest'; // Mock winston to control logging behavior in tests vi.mock('winston', () => { const mockLogger = { log: vi.fn(), info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn(), on: vi.fn((event, callback) => { // Simulate immediate finish for flush tests if (event === 'finish') { setTimeout(callback, 0); } }), end: vi.fn(), level: 'info' }; return { default: { createLogger: vi.fn(() => mockLogger), format: { errors: vi.fn(() => 'errors-format'), timestamp: vi.fn(() => 'timestamp-format'), colorize: vi.fn(() => 'colorize-format'), printf: vi.fn(() => 'printf-format'), json: vi.fn(() => 'json-format'), combine: vi.fn((...formats) => formats.join('-')) }, transports: { Console: vi.fn(), File: vi.fn() } } }; }); import winston from 'winston'; import { Logger } from '../../lib/utils/logger.js'; describe('Logger', () => { let logger; let mockWinston; let mockSecurityLogger; let originalNodeEnv; let originalLogLevel; beforeEach(() => { // Reset mocks vi.clearAllMocks(); // Store original environment variables originalNodeEnv = process.env.NODE_ENV; originalLogLevel = process.env.SQL_SERVER_LOG_LEVEL; // Get mock references - create fresh instances for each test mockWinston = { log: vi.fn(), info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn(), on: vi.fn((event, callback) => { if (event === 'finish') { setTimeout(callback, 0); } }), end: vi.fn(), level: 'info' }; mockSecurityLogger = { log: vi.fn(), info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn(), on: vi.fn((event, callback) => { if (event === 'finish') { setTimeout(callback, 0); } }), end: vi.fn(), level: 'info' }; // Configure winston mock to return fresh instances winston.createLogger.mockImplementation(() => { // Alternate between main and security logger const calls = winston.createLogger.mock.calls.length; return calls % 2 === 1 ? mockWinston : mockSecurityLogger; }); // Mock Date.now for consistent timestamps vi.spyOn(Date.prototype, 'toISOString').mockReturnValue('2023-01-01T00:00:00.000Z'); }); afterEach(() => { // Restore environment variables process.env.NODE_ENV = originalNodeEnv; process.env.SQL_SERVER_LOG_LEVEL = originalLogLevel; // Restore Date mock Date.prototype.toISOString.mockRestore(); }); describe('Constructor and Configuration', () => { test('should initialize with default configuration', () => { logger = new Logger(); expect(logger.config).toEqual({ level: 'info', enableSecurityAudit: true, includeTimestamp: true, includeMetadata: true, maxFileSize: '10m', maxFiles: 5 }); expect(winston.createLogger).toHaveBeenCalled(); // Should create loggers }); test('should initialize with custom configuration', () => { const config = { level: 'debug', enableSecurityAudit: false, includeTimestamp: false, includeMetadata: false, maxFileSize: '5m', maxFiles: 3, customOption: 'test' }; logger = new Logger(config); expect(logger.config).toEqual(config); }); test('should use environment variable for log level', () => { process.env.SQL_SERVER_LOG_LEVEL = 'debug'; logger = new Logger(); expect(logger.config.level).toBe('debug'); }); test('should override environment with explicit config', () => { process.env.SQL_SERVER_LOG_LEVEL = 'debug'; logger = new Logger({ level: 'warn' }); expect(logger.config.level).toBe('warn'); }); test('should create security logger when enabled', () => { logger = new Logger({ enableSecurityAudit: true }); expect(logger.securityLogger).toBe(mockSecurityLogger); expect(winston.createLogger).toHaveBeenCalled(); }); test('should not create security logger when disabled', () => { logger = new Logger({ enableSecurityAudit: false }); expect(logger.securityLogger).toBeNull(); expect(winston.createLogger).toHaveBeenCalled(); }); }); describe('Logger Creation', () => { test('should create logger with development format', () => { // Ensure MCP environment heuristics don't disable color output in dev const mcpSpy = vi.spyOn(Logger.prototype, '_isMcpEnvironment').mockReturnValue(false); process.env.NODE_ENV = 'development'; logger = new Logger(); // Verify development-specific formats were used expect(winston.format.colorize).toHaveBeenCalled(); expect(winston.format.printf).toHaveBeenCalled(); expect(winston.format.json).not.toHaveBeenCalled(); mcpSpy.mockRestore(); }); test('should create logger with production format', () => { process.env.NODE_ENV = 'production'; logger = new Logger(); // Verify production-specific formats were used expect(winston.format.json).toHaveBeenCalled(); expect(winston.format.colorize).not.toHaveBeenCalled(); }); test('should include timestamp format when enabled', () => { logger = new Logger({ includeTimestamp: true }); expect(winston.format.timestamp).toHaveBeenCalled(); }); test('should not include timestamp format when disabled', () => { logger = new Logger({ includeTimestamp: false }); expect(winston.format.timestamp).toHaveBeenCalledTimes(1); // Only for security logger }); test('should create file transport in production with logFile', () => { process.env.NODE_ENV = 'production'; logger = new Logger({ logFile: '/path/to/app.log', securityLogFile: '/path/to/security.log' }); expect(winston.transports.File).toHaveBeenCalledTimes(2); // Main + security file transports }); test('should not create file transport without logFile', () => { process.env.NODE_ENV = 'production'; logger = new Logger(); expect(winston.transports.File).not.toHaveBeenCalled(); }); }); describe('Basic Logging Methods', () => { beforeEach(() => { logger = new Logger(); }); test('should log with metadata when includeMetadata is true', () => { logger.log('info', 'Test message', { key: 'value' }); expect(mockWinston.log).toHaveBeenCalledWith('info', 'Test message', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', key: 'value' }); }); test('should log without metadata when includeMetadata is false', () => { logger = new Logger({ includeMetadata: false }); logger.log('info', 'Test message', { key: 'value' }); expect(mockWinston.log).toHaveBeenCalledWith('info', 'Test message', { key: 'value' }); }); test('should log info messages', () => { logger.info('Info message', { data: 'test' }); expect(mockWinston.log).toHaveBeenCalledWith('info', 'Info message', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', data: 'test' }); }); test('should log warning messages', () => { logger.warn('Warning message', { context: 'test' }); expect(mockWinston.log).toHaveBeenCalledWith('warn', 'Warning message', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', context: 'test' }); }); test('should log debug messages', () => { logger.debug('Debug message', { debug: true }); expect(mockWinston.log).toHaveBeenCalledWith('debug', 'Debug message', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', debug: true }); }); }); describe('Error Logging', () => { beforeEach(() => { logger = new Logger(); }); test('should log errors with Error object', () => { const error = new Error('Test error'); error.stack = 'Error stack trace'; logger.error('Error occurred', error, { context: 'test' }); expect(mockWinston.log).toHaveBeenCalledWith('error', 'Error occurred', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', context: 'test', error: { name: 'Error', message: 'Test error', stack: 'Error stack trace' } }); }); test('should log errors with plain object', () => { const errorData = { code: 'ERR001', details: 'Custom error' }; logger.error('Error occurred', errorData, { context: 'test' }); expect(mockWinston.log).toHaveBeenCalledWith('error', 'Error occurred', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', context: 'test', error: errorData }); }); test('should log errors without error object', () => { logger.error('Error occurred', undefined, { context: 'test' }); expect(mockWinston.log).toHaveBeenCalledWith('error', 'Error occurred', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', context: 'test', error: {} }); }); }); describe('Security Logging', () => { beforeEach(() => { logger = new Logger(); }); test('should log security events when security logger enabled', () => { logger.security('UNAUTHORIZED_ACCESS', 'User attempted unauthorized access', { userId: 'user123', resource: 'sensitive_table' }); expect(mockSecurityLogger.info).toHaveBeenCalledWith('Security Event', { event: 'UNAUTHORIZED_ACCESS', message: 'User attempted unauthorized access', timestamp: '2023-01-01T00:00:00.000Z', severity: 'CRITICAL', userId: 'user123', resource: 'sensitive_table' }); }); test('should log high-severity security events to main logger', () => { logger.security('CONNECTION_FAILED', 'Database connection failed', { host: 'db.example.com' }); expect(mockSecurityLogger.info).toHaveBeenCalled(); expect(mockWinston.log).toHaveBeenCalledWith( 'warn', 'Security Event: Database connection failed', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', securityEvent: expect.objectContaining({ event: 'CONNECTION_FAILED', severity: 'HIGH' }) } ); }); test('should not log low-severity security events to main logger', () => { logger.security('CONFIGURATION_CHANGE', 'Config updated', {}); expect(mockSecurityLogger.info).toHaveBeenCalled(); // Should not call main logger for MEDIUM severity expect(mockWinston.log).toHaveBeenCalledTimes(0); }); test('should handle disabled security logging', () => { logger = new Logger({ enableSecurityAudit: false }); logger.security('UNAUTHORIZED_ACCESS', 'Test event'); expect(mockWinston.log).toHaveBeenCalledWith('warn', 'Security logging is disabled', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', event: 'UNAUTHORIZED_ACCESS', message: 'Test event' }); }); }); describe('Security Severity Mapping', () => { beforeEach(() => { logger = new Logger(); }); test('should return correct severity for known events', () => { expect(logger.getSecuritySeverity('QUERY_BLOCKED')).toBe('MEDIUM'); expect(logger.getSecuritySeverity('CONNECTION_FAILED')).toBe('HIGH'); expect(logger.getSecuritySeverity('UNAUTHORIZED_ACCESS')).toBe('CRITICAL'); expect(logger.getSecuritySeverity('SUSPICIOUS_ACTIVITY')).toBe('HIGH'); expect(logger.getSecuritySeverity('CONFIGURATION_CHANGE')).toBe('MEDIUM'); expect(logger.getSecuritySeverity('PRIVILEGE_ESCALATION')).toBe('CRITICAL'); }); test('should return LOW severity for unknown events', () => { expect(logger.getSecuritySeverity('UNKNOWN_EVENT')).toBe('LOW'); expect(logger.getSecuritySeverity('')).toBe('LOW'); }); }); describe('Query Execution Logging', () => { beforeEach(() => { logger = new Logger(); }); test('should log successful query execution', () => { logger.logQueryExecution( 'execute_query', 'SELECT * FROM users WHERE id = 1', { database: 'testdb', user: 'testuser', securityLevel: 'low' }, { success: true, rowsAffected: 1, duration: 250 } ); expect(mockWinston.log).toHaveBeenCalledWith('info', 'Query executed successfully', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', tool: 'execute_query', query: 'SELECT * FROM users WHERE id = 1', database: 'testdb', user: 'testuser', success: true, rowsAffected: 1, duration: 250, securityLevel: 'low' }); }); test('should log failed query execution', () => { const error = new Error('Syntax error'); logger.logQueryExecution( 'execute_query', 'INVALID SQL', { database: 'testdb' }, { success: false, error, duration: 50 } ); expect(mockWinston.log).toHaveBeenCalledWith('error', 'Query execution failed', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', tool: 'execute_query', query: 'INVALID SQL', database: 'testdb', user: 'unknown', success: false, rowsAffected: 0, duration: 50, securityLevel: 'unknown', error: { name: 'Error', message: 'Syntax error', stack: expect.any(String) } }); }); test('should truncate long queries', () => { const longQuery = 'SELECT * FROM users WHERE ' + 'condition AND '.repeat(20) + 'final_condition'; logger.logQueryExecution('execute_query', longQuery, {}, { success: true }); const logCall = mockWinston.log.mock.calls[0]; expect(logCall[2].query).toHaveLength(203); // 200 chars + '...' expect(logCall[2].query).toMatch(/\.\.\.$/); }); test('should log security violations', () => { logger.logQueryExecution( 'execute_query', 'DROP TABLE users', { securityViolation: true }, { success: false, error: new Error('Blocked by security policy') } ); expect(mockSecurityLogger.info).toHaveBeenCalledWith('Security Event', { event: 'QUERY_BLOCKED', message: 'Query blocked by security policy', timestamp: '2023-01-01T00:00:00.000Z', severity: 'MEDIUM', tool: 'execute_query', reason: 'Blocked by security policy', query: 'DROP TABLE users' }); }); test('should use default values for missing context', () => { logger.logQueryExecution('list_tables', 'SHOW TABLES'); const logCall = mockWinston.log.mock.calls[0]; expect(logCall[2]).toEqual({ service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', tool: 'list_tables', query: 'SHOW TABLES', database: 'default', user: 'unknown', success: true, rowsAffected: 0, duration: 0, securityLevel: 'unknown' }); }); }); describe('Database Connection Logging', () => { beforeEach(() => { logger = new Logger(); }); test('should log successful connection', () => { logger.logConnection('CONNECTION_SUCCESS', { host: 'localhost', database: 'testdb', authType: 'integrated' }); expect(mockWinston.log).toHaveBeenCalledWith('info', 'Database connection established', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', event: 'CONNECTION_SUCCESS', host: 'localhost', database: 'testdb', authType: 'integrated' }); }); test('should log failed connection with security event', () => { const error = new Error('Login failed'); logger.logConnection('CONNECTION_FAILED', { host: 'localhost', database: 'testdb', authType: 'sql', error }); expect(mockWinston.log).toHaveBeenCalledWith('error', 'Database connection failed', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', event: 'CONNECTION_FAILED', host: 'localhost', database: 'testdb', authType: 'sql', error: { name: 'Error', message: 'Login failed', stack: expect.any(String) } }); expect(mockSecurityLogger.info).toHaveBeenCalledWith('Security Event', { event: 'CONNECTION_FAILED', message: 'Database connection failure', timestamp: '2023-01-01T00:00:00.000Z', severity: 'HIGH', host: 'localhost', database: 'testdb', authType: 'sql', error }); }); test('should log connection retry', () => { logger.logConnection('CONNECTION_RETRY', { host: 'localhost', attempt: 2 }); expect(mockWinston.log).toHaveBeenCalledWith('warn', 'Database connection retry', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', event: 'CONNECTION_RETRY', host: 'localhost', database: 'unknown', authType: 'unknown', attempt: 2 }); }); test('should log unknown connection events', () => { logger.logConnection('CUSTOM_EVENT', { custom: 'data' }); expect(mockWinston.log).toHaveBeenCalledWith( 'info', 'Database connection event: CUSTOM_EVENT', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', event: 'CUSTOM_EVENT', host: 'unknown', database: 'unknown', authType: 'unknown', custom: 'data' } ); }); test('should remove sensitive information from logs', () => { logger.logConnection('CONNECTION_SUCCESS', { host: 'localhost', password: 'secret123', connectionString: 'Server=localhost;Password=secret123;', authType: 'sql' }); const logCall = mockWinston.log.mock.calls[0]; expect(logCall[2]).not.toHaveProperty('password'); expect(logCall[2]).not.toHaveProperty('connectionString'); expect(logCall[2]).toHaveProperty('authType', 'sql'); }); }); describe('Tool Usage Logging', () => { beforeEach(() => { logger = new Logger(); }); test('should log tool usage with statistics', () => { logger.logToolUsage('export_table_csv', { executionTime: 1500, resultSize: 102400, cacheHit: true, rowsExported: 500 }); expect(mockWinston.log).toHaveBeenCalledWith('info', 'Tool usage: export_table_csv', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', tool: 'export_table_csv', executionTime: 1500, resultSize: 102400, cacheHit: true, rowsExported: 500 }); }); test('should use default values for missing statistics', () => { logger.logToolUsage('list_tables'); expect(mockWinston.log).toHaveBeenCalledWith('info', 'Tool usage: list_tables', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', tool: 'list_tables', executionTime: 0, resultSize: 0, cacheHit: false }); }); }); describe('Child Logger', () => { beforeEach(() => { logger = new Logger(); }); test('should create child logger with additional context', () => { const childLogger = logger.child({ requestId: 'req-123', userId: 'user-456' }); childLogger.info('Test message', { action: 'test' }); expect(mockWinston.log).toHaveBeenCalledWith('info', 'Test message', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', requestId: 'req-123', userId: 'user-456', action: 'test' }); }); test('should merge context with existing default context', () => { logger.defaultContext = { service: 'parent-service' }; const childLogger = logger.child({ requestId: 'req-123' }); childLogger.info('Test message', { action: 'test' }); expect(mockWinston.log).toHaveBeenCalledWith('info', 'Test message', { service: 'parent-service', // Should use parent service from defaultContext timestamp: '2023-01-01T00:00:00.000Z', requestId: 'req-123', action: 'test' }); }); test('should override parent context with child context', () => { logger.defaultContext = { priority: 'low' }; const childLogger = logger.child({ priority: 'high' }); childLogger.info('Test message'); expect(mockWinston.log).toHaveBeenCalledWith('info', 'Test message', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z', priority: 'high' }); }); }); describe('Log Level Management', () => { beforeEach(() => { logger = new Logger(); }); test('should set new log level', () => { logger.setLevel('debug'); expect(logger.config.level).toBe('debug'); expect(mockWinston.level).toBe('debug'); expect(mockWinston.log).toHaveBeenCalledWith('info', 'Log level changed to debug', { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z' }); }); }); describe('Log Flushing', () => { beforeEach(() => { logger = new Logger(); }); test('should flush logs successfully', async () => { const flushPromise = logger.flush(); // Wait for promise to resolve await expect(flushPromise).resolves.toBeUndefined(); expect(mockWinston.end).toHaveBeenCalled(); expect(mockSecurityLogger.end).toHaveBeenCalled(); expect(mockWinston.on).toHaveBeenCalledWith('finish', expect.any(Function)); expect(mockSecurityLogger.on).toHaveBeenCalledWith('finish', expect.any(Function)); }); test('should flush only main logger when security logger disabled', async () => { logger = new Logger({ enableSecurityAudit: false }); await logger.flush(); expect(mockWinston.end).toHaveBeenCalled(); // When security logger is disabled, mockSecurityLogger.end might still be called // due to our mock setup, so we don't assert on it }); }); describe('Edge Cases and Error Handling', () => { beforeEach(() => { logger = new Logger(); }); test('should handle undefined values gracefully', () => { logger.info(undefined); logger.warn(null); logger.debug('', null); expect(mockWinston.log).toHaveBeenCalledTimes(3); }); test('should handle empty context objects', () => { logger.logQueryExecution('test_tool', 'SELECT 1', {}, {}); logger.logConnection('TEST_EVENT', {}); logger.logToolUsage('test_tool', {}); expect(mockWinston.log).toHaveBeenCalledTimes(3); }); test('should handle circular reference in metadata', () => { const circular = { name: 'test' }; circular.self = circular; // Should not throw expect(() => { logger.info('Test with circular reference', circular); }).not.toThrow(); }); test('should handle very long messages', () => { const longMessage = 'A'.repeat(10000); logger.info(longMessage); expect(mockWinston.log).toHaveBeenCalledWith('info', longMessage, { service: 'warp-sql-server-mcp', timestamp: '2023-01-01T00:00:00.000Z' }); }); }); describe('Environment Detection', () => { beforeEach(() => { logger = new Logger(); }); test('should detect development environment when in project directory', () => { // Since we're running tests in the project directory, this should return true const isDev = logger._isDevelopmentEnvironment(); expect(isDev).toBe(true); }); test('should respect NODE_ENV environment variable', () => { const originalNodeEnv = process.env.NODE_ENV; try { // Test development environment process.env.NODE_ENV = 'development'; const logger1 = new Logger(); expect(logger1._isDevelopmentEnvironment()).toBe(true); // Test test environment process.env.NODE_ENV = 'test'; const logger2 = new Logger(); expect(logger2._isDevelopmentEnvironment()).toBe(true); // Test production environment (should still be true because we're in project dir) process.env.NODE_ENV = 'production'; const logger3 = new Logger(); expect(logger3._isDevelopmentEnvironment()).toBe(true); // Still true due to project detection } finally { process.env.NODE_ENV = originalNodeEnv; } }); test('should provide smart log file defaults', () => { const defaults = logger._getSmartLogDefaults(); expect(defaults).toHaveProperty('logFile'); expect(defaults).toHaveProperty('securityLogFile'); expect(typeof defaults.logFile).toBe('string'); expect(typeof defaults.securityLogFile).toBe('string'); }); test('should use appropriate log paths based on environment', () => { const defaults = logger._getSmartLogDefaults(); if (logger._isDevelopmentEnvironment()) { // In development, should use project directory expect(defaults.logFile).toContain('logs/server.log'); expect(defaults.securityLogFile).toContain('logs/security-audit.log'); } else { // In production, should use user state directory expect(defaults.logFile).toContain('warp-sql-server-mcp'); expect(defaults.securityLogFile).toContain('warp-sql-server-mcp'); } }); }); });

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/egarcia74/warp-sql-server-mcp'

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