Update version to 0.0.16, add Appender and AppenderFactory classes, refactor LoggerFactory methods, and implement logging functionality with tests.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@techniker-me/logger",
|
||||
"version": "0.0.15",
|
||||
"version": "0.0.16",
|
||||
"description": "A logger package for logging",
|
||||
"type": "module",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class LoggerFactory {
|
||||
return LoggerFactory._loggers.get(category) as Logger;
|
||||
}
|
||||
|
||||
public static applyApppender(appender: IAppender): Disposable {
|
||||
public static applyAppender(appender: IAppender): Disposable {
|
||||
LoggerFactory._appenders.add(appender);
|
||||
|
||||
return new Disposable(() => LoggerFactory._appenders.delete(appender));
|
||||
@@ -40,10 +40,10 @@ export default class LoggerFactory {
|
||||
}
|
||||
|
||||
private static applyConsoleAppender(): void {
|
||||
LoggerFactory.applyApppender(new ConsoleAppender());
|
||||
LoggerFactory.applyAppender(new ConsoleAppender());
|
||||
}
|
||||
|
||||
private static applyRemoteAppender(): void {
|
||||
LoggerFactory.applyApppender(new TechnikerMeAppender());
|
||||
LoggerFactory.applyAppender(new TechnikerMeAppender());
|
||||
}
|
||||
}
|
||||
|
||||
62
src/appenders/Appender.ts
Normal file
62
src/appenders/Appender.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import {LoggingLevelType} from '../level/LoggingLevel';
|
||||
import ILogMessage from './LogMessage';
|
||||
|
||||
export type AppenderOptions = {
|
||||
domain?: string;
|
||||
};
|
||||
|
||||
export default class Appender {
|
||||
private readonly _logRecorderUrl: string = 'https://logserver.techniker.me/api/logs';
|
||||
private readonly _domain: string = typeof window !== 'undefined' ? (window.location?.hostname ?? '') : '';
|
||||
private readonly _logMessageQueue: ILogMessage[] = [];
|
||||
private _pendingPostLogMessagePromise: Promise<Response | undefined> | undefined = undefined;
|
||||
|
||||
constructor(logRecorderUrl: string, {domain}: AppenderOptions) {
|
||||
this._logRecorderUrl = logRecorderUrl;
|
||||
this._domain = domain ?? this._domain;
|
||||
}
|
||||
|
||||
public log(timestamp: string, level: LoggingLevelType, category: string, message: string): void {
|
||||
const logMessage = {
|
||||
timestamp,
|
||||
domain: this._domain,
|
||||
level,
|
||||
category,
|
||||
message
|
||||
};
|
||||
this.queueMessage(logMessage);
|
||||
this.postLogMessage();
|
||||
}
|
||||
|
||||
private async postLogMessage(): Promise<void> {
|
||||
const logMessage = this._logMessageQueue.shift();
|
||||
|
||||
if (!logMessage || this._pendingPostLogMessagePromise !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof fetch === 'undefined') {
|
||||
console.error('Fetch API is not available in this environment');
|
||||
return;
|
||||
}
|
||||
|
||||
this._pendingPostLogMessagePromise = fetch(this._logRecorderUrl, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
mode: 'no-cors',
|
||||
method: 'POST',
|
||||
body: JSON.stringify(logMessage)
|
||||
}).then(() => (this._pendingPostLogMessagePromise = undefined));
|
||||
} catch (e) {
|
||||
console.error('Unable to send logs due to [%o]', e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private queueMessage(logMessage: ILogMessage): void {
|
||||
this._logMessageQueue.push(logMessage);
|
||||
}
|
||||
}
|
||||
12
src/appenders/AppenderFactory.ts
Normal file
12
src/appenders/AppenderFactory.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {AppenderOptions} from './Appender';
|
||||
import Appender from './Appender';
|
||||
|
||||
export default class AppnederFactory {
|
||||
public static createRemoteAppender(remoteAppenderUrl: string, {domain}: AppenderOptions): Appender {
|
||||
return new Appender(remoteAppenderUrl, {domain});
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
throw new Error('AppenderFactory is a static class that may not be instantiated');
|
||||
}
|
||||
}
|
||||
6
src/appenders/LogMessage.ts
Normal file
6
src/appenders/LogMessage.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default interface ILogMessage {
|
||||
timestamp: string;
|
||||
level: string;
|
||||
category: string;
|
||||
message: string;
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import type ILogger from './ILogger';
|
||||
import type IAppender from './appenders/IAppender';
|
||||
import AppenderFactory from './appenders/AppenderFactory';
|
||||
import LoggerFactory from './LoggerFactory';
|
||||
import LoggingLevelMapping from './level/LoggingLevelMapping';
|
||||
|
||||
export type {ILogger, IAppender};
|
||||
export {LoggerFactory, LoggingLevelMapping};
|
||||
export default {LoggerFactory, LoggingLevelMapping};
|
||||
export {AppenderFactory, LoggerFactory, LoggingLevelMapping};
|
||||
export default {AppenderFactory, LoggerFactory, LoggingLevelMapping};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export enum LoggingLevel {
|
||||
Off = -1,
|
||||
Info = 10,
|
||||
Warn = 20,
|
||||
Error = 30,
|
||||
Debug = 40,
|
||||
Trace = 50,
|
||||
Silly = 60,
|
||||
All = 100
|
||||
Debug = 10,
|
||||
Trace = 20,
|
||||
Silly = 30,
|
||||
Info = 40,
|
||||
Warn = 50,
|
||||
Error = 60,
|
||||
All = 70
|
||||
}
|
||||
|
||||
export type LoggingLevelType = 'Off' | 'Info' | 'Warn' | 'Error' | 'Debug' | 'Trace' | 'Silly' | 'All';
|
||||
export type LoggingLevelType = 'Off' | 'Debug' | 'Trace' | 'Info' | 'Silly' | 'Warn' | 'Error' | 'All';
|
||||
|
||||
@@ -3,7 +3,7 @@ import Defaults from '../Defaults';
|
||||
import {LoggingLevel} from './LoggingLevel';
|
||||
|
||||
class Threshold {
|
||||
private _threshold: Subject<LoggingLevel> = new Subject(LoggingLevel.Debug);
|
||||
private _threshold: Subject<LoggingLevel> = new Subject(LoggingLevel.Info);
|
||||
|
||||
constructor(loggingLevel?: LoggingLevel) {
|
||||
this._threshold = new Subject(loggingLevel ?? Defaults.loggingLevel);
|
||||
|
||||
78
tests/ConsoleAppender.test.ts
Normal file
78
tests/ConsoleAppender.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'bun:test';
|
||||
import ConsoleAppender from '../src/appenders/ConsoleAppender';
|
||||
import { LoggingLevel } from '../src/level/LoggingLevel';
|
||||
|
||||
describe('ConsoleAppender', () => {
|
||||
let consoleAppender: ConsoleAppender;
|
||||
let mockConsoleLog: any;
|
||||
let mockConsoleError: any;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleAppender = new ConsoleAppender();
|
||||
|
||||
// Mock console methods
|
||||
mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockConsoleLog.mockRestore();
|
||||
mockConsoleError.mockRestore();
|
||||
});
|
||||
|
||||
it('should log info messages to console.log', () => {
|
||||
consoleAppender.log('2023-01-01T00:00:00.000Z', 'Info', 'test-category', 'info message');
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledWith('2023-01-01T00:00:00.000Z [Info] [test-category] info message');
|
||||
expect(mockConsoleError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log debug messages to console.log', () => {
|
||||
consoleAppender.log('2023-01-01T00:00:00.000Z', 'Debug', 'test-category', 'debug message');
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledWith('2023-01-01T00:00:00.000Z [Debug] [test-category] debug message');
|
||||
expect(mockConsoleError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log trace messages to console.log', () => {
|
||||
consoleAppender.log('2023-01-01T00:00:00.000Z', 'Trace', 'test-category', 'trace message');
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledWith('2023-01-01T00:00:00.000Z [Trace] [test-category] trace message');
|
||||
expect(mockConsoleError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log silly messages to console.log', () => {
|
||||
consoleAppender.log('2023-01-01T00:00:00.000Z', 'Silly', 'test-category', 'silly message');
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledWith('2023-01-01T00:00:00.000Z [Silly] [test-category] silly message');
|
||||
expect(mockConsoleError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log warn messages to console.error', () => {
|
||||
consoleAppender.log('2023-01-01T00:00:00.000Z', 'Warn', 'test-category', 'warn message');
|
||||
|
||||
expect(mockConsoleError).toHaveBeenCalledWith('2023-01-01T00:00:00.000Z [Warn] [test-category] warn message');
|
||||
expect(mockConsoleLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log error messages to console.error', () => {
|
||||
consoleAppender.log('2023-01-01T00:00:00.000Z', 'Error', 'test-category', 'error message');
|
||||
|
||||
expect(mockConsoleError).toHaveBeenCalledWith('2023-01-01T00:00:00.000Z [Error] [test-category] error message');
|
||||
expect(mockConsoleLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log all messages to console.log', () => {
|
||||
consoleAppender.log('2023-01-01T00:00:00.000Z', 'All', 'test-category', 'all message');
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledWith('2023-01-01T00:00:00.000Z [All] [test-category] all message');
|
||||
expect(mockConsoleError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not log off messages', () => {
|
||||
consoleAppender.log('2023-01-01T00:00:00.000Z', 'Off', 'test-category', 'off message');
|
||||
|
||||
expect(mockConsoleLog).not.toHaveBeenCalled();
|
||||
expect(mockConsoleError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
227
tests/Logger.test.ts
Normal file
227
tests/Logger.test.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { describe, it, expect, beforeEach, vi, type Mock } from 'bun:test';
|
||||
import Logger from '../src/Logger';
|
||||
import { LoggingLevel } from '../src/level/LoggingLevel';
|
||||
import Threshold from '../src/level/Threshold';
|
||||
import type IAppender from '../src/appenders/IAppender';
|
||||
|
||||
describe('Logger', () => {
|
||||
let mockAppender: IAppender;
|
||||
let threshold: Threshold;
|
||||
let logger: Logger;
|
||||
|
||||
beforeEach(() => {
|
||||
mockAppender = {
|
||||
log: vi.fn(),
|
||||
};
|
||||
|
||||
threshold = new Threshold(LoggingLevel.Info);
|
||||
logger = new Logger('test-category', threshold, new Set([mockAppender]));
|
||||
});
|
||||
|
||||
describe('threshold filtering', () => {
|
||||
it('should log info messages when threshold is Info', () => {
|
||||
logger.info('test message');
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||
expect.any(String), // timestamp
|
||||
'Info',
|
||||
'test-category',
|
||||
'test message'
|
||||
);
|
||||
});
|
||||
|
||||
it('should log warn messages when threshold is Info', () => {
|
||||
logger.warn('test message');
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'Warn',
|
||||
'test-category',
|
||||
'test message'
|
||||
);
|
||||
});
|
||||
|
||||
it('should log error messages when threshold is Info', () => {
|
||||
logger.error('test message');
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'Error',
|
||||
'test-category',
|
||||
'test message'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not log debug messages when threshold is Info', () => {
|
||||
logger.debug('test message');
|
||||
|
||||
expect(mockAppender.log).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not log trace messages when threshold is Info', () => {
|
||||
logger.trace('test message');
|
||||
|
||||
expect(mockAppender.log).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not log silly messages when threshold is Info', () => {
|
||||
logger.silly('test message');
|
||||
|
||||
expect(mockAppender.log).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log all messages when threshold is Debug', () => {
|
||||
threshold.value = LoggingLevel.Debug;
|
||||
|
||||
logger.debug('debug message');
|
||||
logger.info('info message');
|
||||
logger.warn('warn message');
|
||||
logger.error('error message');
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it('should log only error messages when threshold is Error', () => {
|
||||
threshold.value = LoggingLevel.Error;
|
||||
|
||||
logger.error('error message');
|
||||
logger.warn('warn message');
|
||||
logger.info('info message');
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalledTimes(1);
|
||||
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'Error',
|
||||
'test-category',
|
||||
'error message'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('message formatting', () => {
|
||||
beforeEach(() => {
|
||||
threshold.value = LoggingLevel.Debug; // Allow all messages for formatting tests
|
||||
});
|
||||
|
||||
it('should format messages with %s placeholder', () => {
|
||||
logger.info('Hello %s', 'world');
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'Info',
|
||||
'test-category',
|
||||
'Hello world'
|
||||
);
|
||||
});
|
||||
|
||||
it('should format messages with %d placeholder', () => {
|
||||
logger.info('Count: %d', 42);
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'Info',
|
||||
'test-category',
|
||||
'Count: 42'
|
||||
);
|
||||
});
|
||||
|
||||
it('should format messages with %j placeholder', () => {
|
||||
logger.info('Object: %j', { key: 'value' });
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'Info',
|
||||
'test-category',
|
||||
'Object: {"key":"value"}'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle circular references in %j placeholder', () => {
|
||||
const obj: any = { key: 'value' };
|
||||
obj.self = obj;
|
||||
|
||||
logger.info('Circular: %j', obj);
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'Info',
|
||||
'test-category',
|
||||
'Circular: [Circular]'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multiple placeholders', () => {
|
||||
logger.info('User %s has %d items: %j', 'Alice', 5, ['a', 'b']);
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'Info',
|
||||
'test-category',
|
||||
'User Alice has 5 items: ["a","b"]'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle more placeholders than arguments', () => {
|
||||
logger.info('Hello %s %s', 'world');
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'Info',
|
||||
'test-category',
|
||||
'Hello world %s'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle fewer placeholders than arguments', () => {
|
||||
logger.info('Hello %s', 'world', 'extra');
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'Info',
|
||||
'test-category',
|
||||
'Hello world'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle non-number values for %d', () => {
|
||||
logger.info('Value: %d', 'not-a-number');
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'Info',
|
||||
'test-category',
|
||||
'Value: NaN'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timestamp formatting', () => {
|
||||
it('should include ISO timestamp in log messages', () => {
|
||||
const before = new Date().toISOString();
|
||||
logger.info('test');
|
||||
const after = new Date().toISOString();
|
||||
|
||||
const call = (mockAppender.log as Mock).mock.calls[0];
|
||||
const timestamp = call[0];
|
||||
|
||||
expect(timestamp).toBeDefined();
|
||||
expect(() => new Date(timestamp)).not.toThrow();
|
||||
expect(timestamp >= before || timestamp <= after).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple appenders', () => {
|
||||
it('should call all appenders', () => {
|
||||
const mockAppender2: IAppender = {
|
||||
log: vi.fn(),
|
||||
};
|
||||
|
||||
const loggerWithMultiple = new Logger('test', threshold, new Set([mockAppender, mockAppender2]));
|
||||
|
||||
loggerWithMultiple.info('test message');
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalledTimes(1);
|
||||
expect(mockAppender2.log).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
94
tests/LoggerFactory.test.ts
Normal file
94
tests/LoggerFactory.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'bun:test';
|
||||
import LoggerFactory from '../src/LoggerFactory';
|
||||
import { LoggingLevel } from '../src/level/LoggingLevel';
|
||||
import type IAppender from '../src/appenders/IAppender';
|
||||
|
||||
describe('LoggerFactory', () => {
|
||||
let mockAppender: IAppender;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset LoggerFactory state between tests
|
||||
// Note: This is tricky since LoggerFactory uses static state
|
||||
// In a real scenario, we'd need to refactor for better testability
|
||||
mockAppender = {
|
||||
log: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('getLogger', () => {
|
||||
it('should return a logger instance', () => {
|
||||
const logger = LoggerFactory.getLogger('test-category');
|
||||
|
||||
expect(logger).toBeDefined();
|
||||
expect(typeof logger.info).toBe('function');
|
||||
expect(typeof logger.warn).toBe('function');
|
||||
expect(typeof logger.error).toBe('function');
|
||||
expect(typeof logger.debug).toBe('function');
|
||||
});
|
||||
|
||||
it('should return the same logger instance for the same category', () => {
|
||||
const logger1 = LoggerFactory.getLogger('test-category');
|
||||
const logger2 = LoggerFactory.getLogger('test-category');
|
||||
|
||||
expect(logger1).toBe(logger2);
|
||||
});
|
||||
|
||||
it('should return different logger instances for different categories', () => {
|
||||
const logger1 = LoggerFactory.getLogger('category1');
|
||||
const logger2 = LoggerFactory.getLogger('category2');
|
||||
|
||||
expect(logger1).not.toBe(logger2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setLoggingLevel', () => {
|
||||
it('should change the logging threshold', () => {
|
||||
LoggerFactory.setLoggingLevel('Debug');
|
||||
|
||||
// Create a logger and check that debug messages are logged
|
||||
const logger = LoggerFactory.getLogger('test');
|
||||
logger.debug('debug message');
|
||||
|
||||
// Note: We can't easily test this without mocking the appenders
|
||||
// This would require refactoring LoggerFactory for better testability
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyAppender', () => {
|
||||
it('should return a disposable', () => {
|
||||
const disposable = LoggerFactory.applyAppender(mockAppender);
|
||||
|
||||
expect(disposable).toBeDefined();
|
||||
expect(typeof disposable.dispose).toBe('function');
|
||||
});
|
||||
|
||||
it('should add appender to all loggers', () => {
|
||||
const disposable = LoggerFactory.applyAppender(mockAppender);
|
||||
|
||||
const logger = LoggerFactory.getLogger('test');
|
||||
logger.info('test message');
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalled();
|
||||
|
||||
// Clean up
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
it('should remove appender when disposed', () => {
|
||||
const disposable = LoggerFactory.applyAppender(mockAppender);
|
||||
|
||||
const logger = LoggerFactory.getLogger('test');
|
||||
logger.info('test message');
|
||||
|
||||
expect(mockAppender.log).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Dispose and try again
|
||||
disposable.dispose();
|
||||
logger.info('test message 2');
|
||||
|
||||
// Should still be called once since we disposed the mock appender
|
||||
// but the default appenders are still there
|
||||
expect(mockAppender.log).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
94
tests/LoggingLevelMapping.test.ts
Normal file
94
tests/LoggingLevelMapping.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import LoggingLevelMapping from '../src/level/LoggingLevelMapping';
|
||||
import { LoggingLevel, LoggingLevelType } from '../src/level/LoggingLevel';
|
||||
|
||||
describe('LoggingLevelMapping', () => {
|
||||
describe('convertLoggingLevelToLoggingLevelType', () => {
|
||||
it('should convert LoggingLevel.Off to "Off"', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.Off)).toBe('Off');
|
||||
});
|
||||
|
||||
it('should convert LoggingLevel.Debug to "Debug"', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.Debug)).toBe('Debug');
|
||||
});
|
||||
|
||||
it('should convert LoggingLevel.Trace to "Trace"', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.Trace)).toBe('Trace');
|
||||
});
|
||||
|
||||
it('should convert LoggingLevel.Info to "Info"', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.Info)).toBe('Info');
|
||||
});
|
||||
|
||||
it('should convert LoggingLevel.Silly to "Silly"', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.Silly)).toBe('Silly');
|
||||
});
|
||||
|
||||
it('should convert LoggingLevel.Warn to "Warn"', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.Warn)).toBe('Warn');
|
||||
});
|
||||
|
||||
it('should convert LoggingLevel.Error to "Error"', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.Error)).toBe('Error');
|
||||
});
|
||||
|
||||
it('should convert LoggingLevel.All to "All"', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.All)).toBe('All');
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertLoggingLevelTypeToLoggingLevel', () => {
|
||||
it('should convert "Off" to LoggingLevel.Off', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('Off')).toBe(LoggingLevel.Off);
|
||||
});
|
||||
|
||||
it('should convert "Debug" to LoggingLevel.Debug', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('Debug')).toBe(LoggingLevel.Debug);
|
||||
});
|
||||
|
||||
it('should convert "Trace" to LoggingLevel.Trace', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('Trace')).toBe(LoggingLevel.Trace);
|
||||
});
|
||||
|
||||
it('should convert "Info" to LoggingLevel.Info', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('Info')).toBe(LoggingLevel.Info);
|
||||
});
|
||||
|
||||
it('should convert "Silly" to LoggingLevel.Silly', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('Silly')).toBe(LoggingLevel.Silly);
|
||||
});
|
||||
|
||||
it('should convert "Warn" to LoggingLevel.Warn', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('Warn')).toBe(LoggingLevel.Warn);
|
||||
});
|
||||
|
||||
it('should convert "Error" to LoggingLevel.Error', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('Error')).toBe(LoggingLevel.Error);
|
||||
});
|
||||
|
||||
it('should convert "All" to LoggingLevel.All', () => {
|
||||
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('All')).toBe(LoggingLevel.All);
|
||||
});
|
||||
});
|
||||
|
||||
describe('round trip conversion', () => {
|
||||
it('should maintain consistency in round trip conversions', () => {
|
||||
const allLevels: LoggingLevel[] = [
|
||||
LoggingLevel.Off,
|
||||
LoggingLevel.Debug,
|
||||
LoggingLevel.Trace,
|
||||
LoggingLevel.Info,
|
||||
LoggingLevel.Silly,
|
||||
LoggingLevel.Warn,
|
||||
LoggingLevel.Error,
|
||||
LoggingLevel.All
|
||||
];
|
||||
|
||||
for (const level of allLevels) {
|
||||
const type = LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(level);
|
||||
const backToLevel = LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel(type);
|
||||
expect(backToLevel).toBe(level);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
134
tests/TechnikerMeAppender.test.ts
Normal file
134
tests/TechnikerMeAppender.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'bun:test';
|
||||
import TechnikerMeAppender from '../src/appenders/TechnikerMeAppender';
|
||||
|
||||
describe('TechnikerMeAppender', () => {
|
||||
let technikerMeAppender: TechnikerMeAppender;
|
||||
let mockFetch: any;
|
||||
let originalFetch: any;
|
||||
|
||||
beforeEach(() => {
|
||||
technikerMeAppender = new TechnikerMeAppender();
|
||||
|
||||
// Mock fetch
|
||||
originalFetch = global.fetch;
|
||||
mockFetch = vi.fn();
|
||||
global.fetch = mockFetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('log method', () => {
|
||||
it('should queue messages and attempt to post them', () => {
|
||||
mockFetch.mockResolvedValue(new Response());
|
||||
|
||||
technikerMeAppender.log('2023-01-01T00:00:00.000Z', 'Info', 'test-category', 'test message');
|
||||
|
||||
// Should attempt to fetch
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://logserver.techniker.me/api/logs',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
mode: 'no-cors'
|
||||
})
|
||||
);
|
||||
|
||||
const callArgs = mockFetch.mock.calls[0];
|
||||
const body = JSON.parse(callArgs[1].body);
|
||||
|
||||
expect(body).toEqual({
|
||||
timestamp: '2023-01-01T00:00:00.000Z',
|
||||
level: 'Info',
|
||||
category: 'test-category',
|
||||
message: 'test message',
|
||||
domain: '' // Empty since we're in Node.js environment
|
||||
});
|
||||
});
|
||||
|
||||
it('should include domain in browser environment', () => {
|
||||
// Mock window.location
|
||||
const originalWindow = global.window;
|
||||
global.window = {
|
||||
location: {
|
||||
hostname: 'example.com'
|
||||
}
|
||||
} as any;
|
||||
|
||||
const appender = new TechnikerMeAppender();
|
||||
mockFetch.mockResolvedValue(new Response());
|
||||
|
||||
appender.log('2023-01-01T00:00:00.000Z', 'Info', 'test-category', 'test message');
|
||||
|
||||
const callArgs = mockFetch.mock.calls[0];
|
||||
const body = JSON.parse(callArgs[1].body);
|
||||
|
||||
expect(body.domain).toBe('example.com');
|
||||
|
||||
// Restore
|
||||
global.window = originalWindow;
|
||||
});
|
||||
|
||||
it('should handle missing window gracefully', () => {
|
||||
const originalWindow = global.window;
|
||||
delete (global as any).window;
|
||||
|
||||
const appender = new TechnikerMeAppender();
|
||||
mockFetch.mockResolvedValue(new Response());
|
||||
|
||||
appender.log('2023-01-01T00:00:00.000Z', 'Info', 'test-category', 'test message');
|
||||
|
||||
const callArgs = mockFetch.mock.calls[0];
|
||||
const body = JSON.parse(callArgs[1].body);
|
||||
|
||||
expect(body.domain).toBe('');
|
||||
|
||||
// Restore
|
||||
global.window = originalWindow;
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle missing fetch API', () => {
|
||||
const originalFetch = global.fetch;
|
||||
delete (global as any).fetch;
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
technikerMeAppender.log('2023-01-01T00:00:00.000Z', 'Info', 'test-category', 'test message');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Fetch API is not available in this environment');
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
});
|
||||
|
||||
describe('message queuing', () => {
|
||||
it('should attempt to send messages', () => {
|
||||
mockFetch.mockResolvedValue(new Response());
|
||||
|
||||
technikerMeAppender.log('2023-01-01T00:00:00.000Z', 'Info', 'cat1', 'msg1');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://logserver.techniker.me/api/logs',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
mode: 'no-cors'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Note: Complex async queuing behavior is tested by the core functionality test above
|
||||
});
|
||||
});
|
||||
66
tests/Threshold.test.ts
Normal file
66
tests/Threshold.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import Threshold from '../src/level/Threshold';
|
||||
import { LoggingLevel } from '../src/level/LoggingLevel';
|
||||
import Defaults from '../src/Defaults';
|
||||
|
||||
describe('Threshold', () => {
|
||||
describe('constructor', () => {
|
||||
it('should use provided logging level', () => {
|
||||
const threshold = new Threshold(LoggingLevel.Debug);
|
||||
|
||||
expect(threshold.value).toBe(LoggingLevel.Debug);
|
||||
});
|
||||
|
||||
it('should use default logging level when none provided', () => {
|
||||
const threshold = new Threshold();
|
||||
|
||||
expect(threshold.value).toBe(Defaults.loggingLevel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('value getter/setter', () => {
|
||||
it('should get and set threshold value', () => {
|
||||
const threshold = new Threshold(LoggingLevel.Info);
|
||||
|
||||
expect(threshold.value).toBe(LoggingLevel.Info);
|
||||
|
||||
threshold.value = LoggingLevel.Debug;
|
||||
expect(threshold.value).toBe(LoggingLevel.Debug);
|
||||
|
||||
threshold.value = LoggingLevel.Error;
|
||||
expect(threshold.value).toBe(LoggingLevel.Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('threshold comparison logic', () => {
|
||||
it('should work correctly for different levels', () => {
|
||||
const threshold = new Threshold(LoggingLevel.Info);
|
||||
|
||||
// Info level (30) should allow Info (30), Warn (50), Error (60) but not Debug (10)
|
||||
expect(threshold.value > LoggingLevel.Info).toBe(false); // 30 > 30 = false, so Info logs
|
||||
expect(threshold.value > LoggingLevel.Warn).toBe(false); // 30 > 50 = false, so Warn logs
|
||||
expect(threshold.value > LoggingLevel.Error).toBe(false); // 30 > 60 = false, so Error logs
|
||||
expect(threshold.value > LoggingLevel.Debug).toBe(true); // 30 > 10 = true, so Debug doesn't log
|
||||
});
|
||||
|
||||
it('should work with Debug threshold', () => {
|
||||
const threshold = new Threshold(LoggingLevel.Debug);
|
||||
|
||||
// Debug level (10) should allow all levels
|
||||
expect(threshold.value > LoggingLevel.Debug).toBe(false); // 10 > 10 = false
|
||||
expect(threshold.value > LoggingLevel.Info).toBe(false); // 10 > 30 = false
|
||||
expect(threshold.value > LoggingLevel.Warn).toBe(false); // 10 > 50 = false
|
||||
expect(threshold.value > LoggingLevel.Error).toBe(false); // 10 > 60 = false
|
||||
});
|
||||
|
||||
it('should work with Error threshold', () => {
|
||||
const threshold = new Threshold(LoggingLevel.Error);
|
||||
|
||||
// Error level (60) should only allow Error and above
|
||||
expect(threshold.value > LoggingLevel.Error).toBe(false); // 60 > 60 = false, so Error logs
|
||||
expect(threshold.value > LoggingLevel.Warn).toBe(true); // 60 > 50 = true, so Warn doesn't log
|
||||
expect(threshold.value > LoggingLevel.Info).toBe(true); // 60 > 30 = true, so Info doesn't log
|
||||
expect(threshold.value > LoggingLevel.Debug).toBe(true); // 60 > 10 = true, so Debug doesn't log
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user