UNPKG

@seawingai/winglog

Version:

A powerful TypeScript/JavaScript logging library built on top of Pino for structured logging with enhanced features

386 lines (306 loc) 14.5 kB
import { WingLog, LogType } from './winglog'; import * as fs from 'fs'; import * as path from 'path'; import pino from 'pino'; // Mock pino to avoid actual file operations during tests jest.mock('pino'); jest.mock('fs'); jest.mock('path'); const mockPino = pino as jest.Mocked<typeof pino>; const mockFs = fs as jest.Mocked<typeof fs>; const mockPath = path as jest.Mocked<typeof path>; describe('WingLog', () => { let wingLog: WingLog; let mockLogger: any; let mockStream: any; beforeEach(() => { // Reset all mocks jest.clearAllMocks(); // Setup mock logger mockLogger = { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), }; mockStream = { write: jest.fn(), }; // Mock pino methods (mockPino as any).mockReturnValue(mockLogger); (mockPino.destination as any) = jest.fn().mockReturnValue(mockStream); (mockPino.transport as any) = jest.fn().mockReturnValue(mockStream); (mockPino.multistream as any) = jest.fn().mockReturnValue(mockStream); // Mock fs methods mockFs.existsSync.mockReturnValue(false); mockFs.mkdirSync.mockImplementation(() => undefined); // Mock path methods mockPath.join.mockImplementation((...args) => args.join('/')); mockPath.resolve.mockImplementation((...args) => args.join('/')); // Mock process.cwd Object.defineProperty(process, 'cwd', { value: jest.fn().mockReturnValue('/test/cwd'), writable: true, }); wingLog = new WingLog('test-logger'); }); afterEach(() => { jest.restoreAllMocks(); }); describe('Constructor', () => { it('should create a WingLog instance with the given name', () => { expect(wingLog).toBeInstanceOf(WingLog); }); it('should create logs directory if it does not exist', () => { expect(mockFs.existsSync).toHaveBeenCalledWith('/test/cwd/logs'); expect(mockFs.mkdirSync).toHaveBeenCalledWith('/test/cwd/logs', { recursive: true }); }); it('should create logs directory even if it already exists (mkdirSync handles this)', () => { mockFs.existsSync.mockReturnValue(true); new WingLog('test-logger-2'); expect(mockFs.mkdirSync).toHaveBeenCalledWith('/test/cwd/logs', { recursive: true }); }); it('should initialize pino logger with correct configuration', () => { expect(mockPino).toHaveBeenCalledWith( { level: 'debug', timestamp: expect.any(Function), }, mockStream ); }); it('should log initialization message', () => { expect(mockLogger.debug).toHaveBeenCalledWith('Logger initialized: [test-logger]'); }); }); describe('Logging Methods', () => { beforeEach(() => { // Mock Date.now for consistent testing jest.spyOn(Date, 'now').mockReturnValue(1000000); }); it('should log started message correctly', () => { const result = wingLog.started('Test started'); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Test started'); expect(result).toBe(0); }); it('should log finished message correctly', () => { const result = wingLog.finished('Test finished'); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Test finished'); expect(result).toBe(0); }); it('should log success message correctly', () => { const result = wingLog.success('Test success'); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Test success'); expect(result).toBe(0); }); it('should log failed message correctly', () => { const result = wingLog.failed('Test failed'); expect(mockLogger.error).toHaveBeenCalledWith('[test-logger]:Test failed'); expect(result).toBe(0); }); it('should log info message correctly', () => { const result = wingLog.info('Test info'); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Test info'); expect(result).toBe(0); }); it('should log warn message correctly', () => { const result = wingLog.warn('Test warning'); expect(mockLogger.warn).toHaveBeenCalledWith('[test-logger]:Test warning'); expect(result).toBe(0); }); it('should log debug message correctly', () => { const result = wingLog.debug('Test debug'); expect(mockLogger.debug).toHaveBeenCalledWith('[test-logger]:Test debug'); expect(result).toBe(0); }); }); describe('Duration Calculation', () => { beforeEach(() => { // Mock Date.now to return a fixed timestamp jest.spyOn(Date, 'now').mockReturnValue(1000000); }); it('should calculate duration correctly when startTime is provided', () => { const startTime = 999000; // 1 second ago const result = wingLog.started('Test with duration', startTime); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Duration:[00:01]:Test with duration'); expect(result).toBe(1); }); it('should handle zero duration correctly', () => { const startTime = 1000000; // Same time const result = wingLog.started('Test with zero duration', startTime); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Duration:[00:00]:Test with zero duration'); expect(result).toBe(0); }); it('should handle duration with minutes and seconds', () => { const startTime = 940000; // 60 seconds ago const result = wingLog.started('Test with minutes', startTime); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Duration:[01:00]:Test with minutes'); expect(result).toBe(60); }); it('should handle duration with decimal seconds', () => { const startTime = 999500; // 0.5 seconds ago const result = wingLog.started('Test with decimal duration', startTime); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Duration:[00:01]:Test with decimal duration'); expect(result).toBe(0.5); }); it('should handle very large duration values', () => { const startTime = 1; // Very long time ago (but not 0 to avoid falsy check) const result = wingLog.started('Long running task', startTime); expect(result).toBe(1000); // 999.99 seconds rounded to 1000.00 by toFixed(2) expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Duration:[16:40]:Long running task'); }); }); describe('Error Handling', () => { it('should handle Error objects correctly', () => { const error = new Error('Test error message'); wingLog.error('Operation failed', error); expect(mockLogger.error).toHaveBeenCalledWith('[test-logger]:Operation failed: Test error message {"error":{}}'); }); it('should handle non-Error objects correctly', () => { const error = 'String error'; wingLog.error('Operation failed', error); expect(mockLogger.error).toHaveBeenCalledWith('[test-logger]:Operation failed: String error {"error":{}}'); }); it('should handle null/undefined errors', () => { wingLog.error('Operation failed', null); expect(mockLogger.error).toHaveBeenCalledWith('[test-logger]:Operation failed: null {"error":{}}'); }); }); describe('Overloaded Methods', () => { beforeEach(() => { // Mock Date.now to return a fixed timestamp jest.spyOn(Date, 'now').mockReturnValue(1000000); }); it('should handle info with simple message', () => { const result = wingLog.info('Simple message'); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Simple message'); expect(result).toBe(0); }); it('should handle info with structured data', () => { const record = { userId: 123, action: 'login' }; wingLog.info('User action', record); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:User action {"userId":123,"action":"login"}'); }); it('should handle debug with simple message', () => { const result = wingLog.debug('Simple debug message'); expect(mockLogger.debug).toHaveBeenCalledWith('[test-logger]:Simple debug message'); expect(result).toBe(0); }); it('should handle debug with structured data', () => { const record = { query: 'SELECT * FROM users', duration: 45 }; wingLog.debug('Database query', record); expect(mockLogger.debug).toHaveBeenCalledWith('[test-logger]:Database query {"query":"SELECT * FROM users","duration":45}'); }); it('should handle failed with simple message', () => { const result = wingLog.failed('Simple error message'); expect(mockLogger.error).toHaveBeenCalledWith('[test-logger]:Simple error message'); expect(result).toBe(0); }); it('should handle failed with structured data', () => { const record = { userId: 123, reason: 'Invalid token', ip: '192.168.1.1' }; wingLog.failed('Authentication failed', record); expect(mockLogger.error).toHaveBeenCalledWith('[test-logger]:Authentication failed {"userId":123,"reason":"Invalid token","ip":"192.168.1.1"}'); }); it('should handle warn with simple message', () => { const result = wingLog.warn('Simple warning message'); expect(mockLogger.warn).toHaveBeenCalledWith('[test-logger]:Simple warning message'); expect(result).toBe(0); }); it('should handle warn with structured data', () => { const record = { memoryUsage: '85%', threshold: '80%' }; wingLog.warn('High memory usage', record); expect(mockLogger.warn).toHaveBeenCalledWith('[test-logger]:High memory usage {"memoryUsage":"85%","threshold":"80%"}'); }); it('should handle success with simple message', () => { const result = wingLog.success('Simple success message'); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Simple success message'); expect(result).toBe(0); }); it('should handle success with structured data', () => { const record = { userId: 123, email: 'user@example.com' }; wingLog.success('User created successfully', record); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:User created successfully {"userId":123,"email":"user@example.com"}'); }); it('should handle started with simple message', () => { const result = wingLog.started('Simple start message'); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Simple start message'); expect(result).toBe(0); }); it('should handle started with structured data', () => { const record = { jobId: 'job-123', priority: 'high' }; wingLog.started('Job processing started', record); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Job processing started {"jobId":"job-123","priority":"high"}'); }); it('should handle finished with simple message', () => { const result = wingLog.finished('Simple finish message'); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Simple finish message'); expect(result).toBe(0); }); it('should handle finished with structured data', () => { const record = { jobId: 'job-123', duration: 45, status: 'completed' }; wingLog.finished('Job processing finished', record); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Job processing finished {"jobId":"job-123","duration":45,"status":"completed"}'); }); it('should handle duration calculation with simple message', () => { const startTime = 999000; // 1 second ago const result = wingLog.info('Message with duration', startTime); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Duration:[00:01]:Message with duration'); expect(result).toBe(1); }); it('should handle duration calculation with structured data', () => { const record = { userId: 123, action: 'login' }; wingLog.info('User action with duration', record); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:User action with duration {"userId":123,"action":"login"}'); }); }); describe('LogType Enum', () => { it('should have all expected log types', () => { expect(LogType.FAILED).toBe('FAILED'); expect(LogType.WARN).toBe('WARN'); expect(LogType.DEBUG).toBe('DEBUG'); expect(LogType.SUCCESS).toBe('SUCCESS'); expect(LogType.STARTED).toBe('STARTED'); expect(LogType.FINISHED).toBe('FINISHED'); expect(LogType.INFO).toBe('INFO'); }); }); describe('Edge Cases', () => { it('should handle empty messages', () => { wingLog.info(''); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:'); }); it('should handle messages with special characters', () => { wingLog.info('Message with "quotes" and \'apostrophes\''); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Message with "quotes" and \'apostrophes\''); }); it('should handle pino transport fallback', () => { // Mock pino.transport to throw an error (mockPino.transport as any).mockImplementation(() => { throw new Error('Transport not available'); }); // Should not throw when creating new instance expect(() => new WingLog('fallback-test')).not.toThrow(); }); }); describe('Integration Tests', () => { it('should work with multiple log calls', () => { wingLog.started('Task started'); wingLog.info('Task in progress'); wingLog.success('Task completed'); expect(mockLogger.info).toHaveBeenCalledTimes(3); expect(mockLogger.info).toHaveBeenNthCalledWith(1, '[test-logger]:Task started'); expect(mockLogger.info).toHaveBeenNthCalledWith(2, '[test-logger]:Task in progress'); expect(mockLogger.info).toHaveBeenNthCalledWith(3, '[test-logger]:Task completed'); }); it('should handle mixed log levels', () => { wingLog.debug('Debug message'); wingLog.info('Info message'); wingLog.warn('Warning message'); wingLog.error('Error message', new Error('Test error')); expect(mockLogger.debug).toHaveBeenCalledWith('[test-logger]:Debug message'); expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Info message'); expect(mockLogger.warn).toHaveBeenCalledWith('[test-logger]:Warning message'); expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('[test-logger]:Error message')); }); }); });