Skip to main content
Glama
logger.test.ts10.1 kB
/** * @fileOverview: Unit tests for the Logger utility * @module: Logger Tests * @description: Comprehensive test suite for Logger class covering console logging, file logging, log rotation, and error handling */ import * as fs from 'fs'; import * as path from 'path'; import { Logger, LogContext } from '../logger'; // Mock fs module jest.mock('fs', () => ({ existsSync: jest.fn(), mkdirSync: jest.fn(), writeFileSync: jest.fn(), appendFileSync: jest.fn(), statSync: jest.fn(), unlinkSync: jest.fn(), renameSync: jest.fn(), })); // Mock path module jest.mock('path', () => ({ join: jest.fn(), })); const mockFs = fs as jest.Mocked<typeof fs>; const mockPath = path as jest.Mocked<typeof path>; describe('Logger', () => { let logger: Logger; let consoleSpy: jest.SpyInstance; beforeEach(() => { jest.clearAllMocks(); logger = new Logger('Test'); // Mock environment process.env.USERPROFILE = '/home/testuser'; process.env.NODE_ENV = 'test'; // Mock path.join to return predictable paths mockPath.join.mockImplementation((...args: string[]) => args.join('/')); // Mock fs operations mockFs.existsSync.mockReturnValue(true); mockFs.statSync.mockReturnValue({ size: 1000, isFile: () => true, isDirectory: () => false, mtime: new Date(), ctime: new Date(), atime: new Date(), birthtime: new Date(), mode: 0o666, uid: 0, gid: 0, nlink: 1, dev: 0, ino: 0, rdev: 0, blksize: 4096, blocks: 0, } as any); }); afterEach(() => { delete process.env.USERPROFILE; delete process.env.NODE_ENV; delete process.env.DEBUG; }); describe('Constructor', () => { test('should initialize with default prefix', () => { const defaultLogger = new Logger(); expect(defaultLogger).toBeDefined(); }); test('should initialize with custom prefix', () => { expect(logger).toBeDefined(); }); test('should initialize file logging when directory exists', () => { expect(mockFs.mkdirSync).not.toHaveBeenCalled(); expect(mockFs.writeFileSync).toHaveBeenCalledWith( '/home/testuser/.ambiance/logs/mcp-proxy.log', '', { encoding: 'utf8' } ); }); test('should handle file logging initialization failure gracefully', () => { mockFs.existsSync.mockReturnValue(false); mockFs.mkdirSync.mockImplementation(() => { throw new Error('Permission denied'); }); const newLogger = new Logger('Test2'); expect(newLogger).toBeDefined(); // Should not throw }); }); describe('Logging Methods', () => { beforeEach(() => { consoleSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); }); afterEach(() => { consoleSpy.mockRestore(); }); test('should log info messages', () => { logger.info('Test message'); expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining('[INFO] [Test] Test message'), '' ); }); test('should log warn messages', () => { const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); logger.warn('Warning message'); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining('[WARN] [Test] Warning message'), '' ); warnSpy.mockRestore(); }); test('should log error messages', () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); logger.error('Error message'); expect(errorSpy).toHaveBeenCalledWith( expect.stringContaining('[ERROR] [Test] Error message'), '' ); errorSpy.mockRestore(); }); test('should log debug messages when DEBUG is set', () => { process.env.DEBUG = 'true'; const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {}); logger.debug('Debug message'); expect(debugSpy).toHaveBeenCalledWith( expect.stringContaining('[DEBUG] [Test] Debug message'), '' ); debugSpy.mockRestore(); delete process.env.DEBUG; }); test('should not log debug messages when DEBUG is not set', () => { const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {}); logger.debug('Debug message'); expect(debugSpy).not.toHaveBeenCalled(); debugSpy.mockRestore(); }); test('should include context in log messages', () => { const context: LogContext = { userId: 123, action: 'login' }; logger.info('User action', context); expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining('[INFO] [Test] User action'), JSON.stringify(context) ); }); test('should handle empty context', () => { logger.info('No context'); expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining('[INFO] [Test] No context'), '' ); }); }); describe('File Logging', () => { test('should write to log file when available', () => { logger.info('File test message'); expect(mockFs.appendFileSync).toHaveBeenCalledWith( '/home/testuser/.ambiance/logs/mcp-proxy.log', expect.stringContaining('"level":"INFO"'), { encoding: 'utf8' } ); }); test('should handle file write errors gracefully', () => { mockFs.appendFileSync.mockImplementation(() => { throw new Error('Write failed'); }); // Should not throw expect(() => logger.info('Test')).not.toThrow(); }); }); describe('Log Rotation', () => { test('should rotate logs when file exceeds max size', () => { // Mock large file mockFs.statSync.mockReturnValue({ size: 15 * 1024 * 1024, // 15MB > 10MB limit isFile: () => true, isDirectory: () => false, mtime: new Date(), ctime: new Date(), atime: new Date(), birthtime: new Date(), mode: 0o666, uid: 0, gid: 0, nlink: 1, dev: 0, ino: 0, rdev: 0, blksize: 4096, blocks: 0, } as any); logger.info('Rotation test'); // Should rotate existing logs and create new file expect(mockFs.renameSync).toHaveBeenCalledWith( '/home/testuser/.ambiance/logs/mcp-proxy.log', '/home/testuser/.ambiance/logs/mcp-proxy.log.1' ); expect(mockFs.writeFileSync).toHaveBeenCalledWith( '/home/testuser/.ambiance/logs/mcp-proxy.log', '', { encoding: 'utf8' } ); }); test('should handle rotation errors gracefully', () => { mockFs.statSync.mockReturnValue({ size: 15 * 1024 * 1024, isFile: () => true, isDirectory: () => false, mtime: new Date(), ctime: new Date(), atime: new Date(), birthtime: new Date(), mode: 0o666, uid: 0, gid: 0, nlink: 1, dev: 0, ino: 0, rdev: 0, blksize: 4096, blocks: 0, } as any); mockFs.renameSync.mockImplementation(() => { throw new Error('Rename failed'); }); // Should not throw expect(() => logger.info('Test')).not.toThrow(); }); test('should not rotate when file size is below limit', () => { logger.info('No rotation needed'); expect(mockFs.renameSync).not.toHaveBeenCalled(); }); }); describe('Log Entry Format', () => { test('should format log entries as valid JSON', () => { logger.info('JSON test'); const appendCall = mockFs.appendFileSync.mock.calls[0]; const logEntry = appendCall[1] as string; expect(() => JSON.parse(logEntry.trim())).not.toThrow(); const parsed = JSON.parse(logEntry.trim()); expect(parsed).toHaveProperty('timestamp'); expect(parsed).toHaveProperty('level', 'INFO'); expect(parsed).toHaveProperty('prefix', 'Test'); expect(parsed).toHaveProperty('message', 'JSON test'); }); test('should include ISO timestamp', () => { const before = new Date(); logger.info('Timestamp test'); const after = new Date(); const appendCall = mockFs.appendFileSync.mock.calls[0]; const logEntry = JSON.parse((appendCall[1] as string).trim()); const timestamp = new Date(logEntry.timestamp); expect(timestamp.getTime()).toBeGreaterThanOrEqual(before.getTime()); expect(timestamp.getTime()).toBeLessThanOrEqual(after.getTime()); }); }); describe('Environment-Specific Behavior', () => { test('should use console.info for INFO level in test environment', () => { process.env.NODE_ENV = 'test'; const infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); logger.info('Test env info'); expect(infoSpy).toHaveBeenCalled(); expect(errorSpy).not.toHaveBeenCalled(); infoSpy.mockRestore(); errorSpy.mockRestore(); }); test('should use console.debug for DEBUG level in test environment', () => { process.env.NODE_ENV = 'test'; process.env.DEBUG = 'true'; const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {}); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); logger.debug('Test env debug'); expect(debugSpy).toHaveBeenCalled(); expect(errorSpy).not.toHaveBeenCalled(); debugSpy.mockRestore(); errorSpy.mockRestore(); }); test('should use console.error for INFO level in production environment', () => { process.env.NODE_ENV = 'production'; const infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {}); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); logger.info('Prod env info'); expect(infoSpy).not.toHaveBeenCalled(); expect(errorSpy).toHaveBeenCalled(); infoSpy.mockRestore(); errorSpy.mockRestore(); }); }); });

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/sbarron/AmbianceMCP'

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