You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
822 lines
26 KiB
822 lines
26 KiB
/**
|
|
* logr — test suite
|
|
*
|
|
* Ejecutar: npx vitest run
|
|
* Cobertura: npx vitest run --coverage
|
|
*
|
|
* Módulos cubiertos:
|
|
* - types (LogLevel, LogEntry, LogFilters, etc.)
|
|
* - engine (createLogr, log methods, serialize)
|
|
* - transports (console, http, callback)
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { createLing } from '../../ling/lib/engine';
|
|
import { createLogr } from '../lib/engine';
|
|
import { consoleTransport, httpTransport, callbackTransport } from '../lib/transports';
|
|
import { LogLevel } from '../lib/types';
|
|
|
|
// ============================================================================
|
|
// FIXTURES
|
|
// ============================================================================
|
|
|
|
const ling = createLing({
|
|
common: {
|
|
ok: { es: 'Aceptar', en: 'OK' },
|
|
error: { es: 'Error', en: 'Error' },
|
|
},
|
|
}, 'es');
|
|
|
|
// ============================================================================
|
|
// LOG LEVEL
|
|
// ============================================================================
|
|
|
|
describe('LogLevel', () => {
|
|
it('los valores numéricos son correctos', () => {
|
|
expect(LogLevel.DEBUG).toBe(0);
|
|
expect(LogLevel.INFO).toBe(1);
|
|
expect(LogLevel.WARN).toBe(2);
|
|
expect(LogLevel.ERROR).toBe(3);
|
|
expect(LogLevel.NONE).toBe(999);
|
|
});
|
|
|
|
it('permite comparar niveles correctamente', () => {
|
|
expect(LogLevel.DEBUG < LogLevel.INFO).toBe(true);
|
|
expect(LogLevel.INFO < LogLevel.WARN).toBe(true);
|
|
expect(LogLevel.WARN < LogLevel.ERROR).toBe(true);
|
|
expect(LogLevel.ERROR < LogLevel.NONE).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// ENGINE — createLing
|
|
// ============================================================================
|
|
|
|
describe('createLogr — nivel por defecto', () => {
|
|
let captured: string[] = [];
|
|
|
|
beforeEach(() => {
|
|
captured = [];
|
|
});
|
|
|
|
it('crea una instancia con nivel WARN por defecto', () => {
|
|
const logr = createLogr(ling, {
|
|
transports: [callbackTransport((_, msg) => captured.push(msg))],
|
|
});
|
|
|
|
logr.debug('test', 'debug msg');
|
|
logr.info('test', 'info msg');
|
|
logr.warn('test', 'warn msg');
|
|
|
|
expect(captured).toHaveLength(1);
|
|
expect(captured[0]).toBe('warn msg');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// ENGINE — métodos de logging
|
|
// ============================================================================
|
|
|
|
describe('createLogr — debug(), info(), warn(), error()', () => {
|
|
let captured: Array<{ level: number; category: string; message: string; context?: Record<string, unknown> }>;
|
|
|
|
beforeEach(() => {
|
|
captured = [];
|
|
ling.setLocale('es'); // Reset locale antes de cada test
|
|
});
|
|
|
|
it('registra mensajes de DEBUG', () => {
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.DEBUG,
|
|
transports: [callbackTransport((entry, msg) => {
|
|
captured.push({ level: entry.level, category: entry.category, message: msg });
|
|
})],
|
|
});
|
|
|
|
logr.debug('auth', 'Debug message');
|
|
|
|
expect(captured).toHaveLength(1);
|
|
expect(captured[0].level).toBe(LogLevel.DEBUG);
|
|
expect(captured[0].category).toBe('auth');
|
|
expect(captured[0].message).toBe('Debug message');
|
|
});
|
|
|
|
it('registra mensajes de INFO', () => {
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.INFO,
|
|
transports: [callbackTransport((entry, msg) => {
|
|
captured.push({ level: entry.level, category: entry.category, message: msg });
|
|
})],
|
|
});
|
|
|
|
logr.info('db', 'Info message');
|
|
|
|
expect(captured).toHaveLength(1);
|
|
expect(captured[0].level).toBe(LogLevel.INFO);
|
|
expect(captured[0].category).toBe('db');
|
|
});
|
|
|
|
it('registra mensajes de WARN', () => {
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.WARN,
|
|
transports: [callbackTransport((entry, msg) => {
|
|
captured.push({ level: entry.level, category: entry.category, message: msg });
|
|
})],
|
|
});
|
|
|
|
logr.warn('api', 'Warn message');
|
|
|
|
expect(captured).toHaveLength(1);
|
|
expect(captured[0].level).toBe(LogLevel.WARN);
|
|
expect(captured[0].category).toBe('api');
|
|
});
|
|
|
|
it('registra mensajes de ERROR', () => {
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.ERROR,
|
|
transports: [callbackTransport((entry, msg) => {
|
|
captured.push({ level: entry.level, category: entry.category, message: msg });
|
|
})],
|
|
});
|
|
|
|
logr.error('payment', 'Error message');
|
|
|
|
expect(captured).toHaveLength(1);
|
|
expect(captured[0].level).toBe(LogLevel.ERROR);
|
|
expect(captured[0].category).toBe('payment');
|
|
});
|
|
|
|
it('incluye contexto cuando se proporciona', () => {
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.INFO,
|
|
transports: [callbackTransport((entry, msg) => {
|
|
captured.push({ level: entry.level, category: entry.category, message: msg, context: entry.context });
|
|
})],
|
|
});
|
|
|
|
const contextData = { userId: 123, action: 'login' };
|
|
logr.info('auth', 'User logged in', contextData);
|
|
|
|
expect(captured).toHaveLength(1);
|
|
expect(captured[0].context).toEqual(contextData);
|
|
});
|
|
|
|
it('resuelve LingRecord al locale activo', () => {
|
|
const msgCaptured: string[] = [];
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.INFO,
|
|
transports: [callbackTransport((_, msg) => msgCaptured.push(msg))],
|
|
});
|
|
|
|
logr.info('test', { es: 'Hola', en: 'Hello' });
|
|
|
|
expect(msgCaptured[0]).toBe('Hola');
|
|
});
|
|
|
|
it('cambia el locale y resuelve correctamente', () => {
|
|
const msgCaptured: string[] = [];
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.INFO,
|
|
transports: [callbackTransport((_, msg) => msgCaptured.push(msg))],
|
|
});
|
|
|
|
logr.info('test', { es: 'Aceptar', en: 'OK' });
|
|
expect(msgCaptured[0]).toBe('Aceptar');
|
|
|
|
ling.setLocale('en');
|
|
logr.info('test', { es: 'Aceptar', en: 'OK' });
|
|
expect(msgCaptured[1]).toBe('OK');
|
|
});
|
|
|
|
it('resuelve referencias IDLing', () => {
|
|
const msgCaptured: string[] = [];
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.INFO,
|
|
transports: [callbackTransport((_, msg) => msgCaptured.push(msg))],
|
|
});
|
|
|
|
logr.info('test', '#?common.ok');
|
|
|
|
expect(msgCaptured[0]).toBe('Aceptar');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// ENGINE — filtrado por nivel
|
|
// ============================================================================
|
|
|
|
describe('createLogr — filtrado por nivel', () => {
|
|
let captured: string[];
|
|
|
|
beforeEach(() => {
|
|
captured = [];
|
|
});
|
|
|
|
it('DEBUG permite todos los niveles', () => {
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.DEBUG,
|
|
transports: [callbackTransport((_, msg) => captured.push(msg))],
|
|
});
|
|
|
|
logr.debug('test', 'd');
|
|
logr.info('test', 'i');
|
|
logr.warn('test', 'w');
|
|
logr.error('test', 'e');
|
|
|
|
expect(captured).toHaveLength(4);
|
|
});
|
|
|
|
it('INFO filtra DEBUG', () => {
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.INFO,
|
|
transports: [callbackTransport((_, msg) => captured.push(msg))],
|
|
});
|
|
|
|
logr.debug('test', 'd');
|
|
logr.info('test', 'i');
|
|
logr.warn('test', 'w');
|
|
logr.error('test', 'e');
|
|
|
|
expect(captured).toHaveLength(3);
|
|
expect(captured).toEqual(['i', 'w', 'e']);
|
|
});
|
|
|
|
it('WARN filtra DEBUG e INFO', () => {
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.WARN,
|
|
transports: [callbackTransport((_, msg) => captured.push(msg))],
|
|
});
|
|
|
|
logr.debug('test', 'd');
|
|
logr.info('test', 'i');
|
|
logr.warn('test', 'w');
|
|
logr.error('test', 'e');
|
|
|
|
expect(captured).toHaveLength(2);
|
|
expect(captured).toEqual(['w', 'e']);
|
|
});
|
|
|
|
it('ERROR filtra DEBUG, INFO y WARN', () => {
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.ERROR,
|
|
transports: [callbackTransport((_, msg) => captured.push(msg))],
|
|
});
|
|
|
|
logr.debug('test', 'd');
|
|
logr.info('test', 'i');
|
|
logr.warn('test', 'w');
|
|
logr.error('test', 'e');
|
|
|
|
expect(captured).toHaveLength(1);
|
|
expect(captured).toEqual(['e']);
|
|
});
|
|
|
|
it('NONE filtra todo', () => {
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.NONE,
|
|
transports: [callbackTransport((_, msg) => captured.push(msg))],
|
|
});
|
|
|
|
logr.debug('test', 'd');
|
|
logr.info('test', 'i');
|
|
logr.warn('test', 'w');
|
|
logr.error('test', 'e');
|
|
|
|
expect(captured).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// ENGINE — historial
|
|
// ============================================================================
|
|
|
|
describe('createLogr — getLogs()', () => {
|
|
it('devuelve todas las entradas sin filtros', () => {
|
|
const logr = createLogr(ling, { level: LogLevel.DEBUG });
|
|
|
|
logr.debug('cat1', 'msg1');
|
|
logr.info('cat2', 'msg2');
|
|
logr.warn('cat1', 'msg3');
|
|
|
|
const logs = logr.getLogs();
|
|
expect(logs).toHaveLength(3);
|
|
});
|
|
|
|
it('filtra por nivel exacto', () => {
|
|
const logr = createLogr(ling, { level: LogLevel.DEBUG });
|
|
|
|
logr.debug('cat1', 'msg1');
|
|
logr.info('cat2', 'msg2');
|
|
logr.debug('cat1', 'msg3');
|
|
|
|
const logs = logr.getLogs({ level: LogLevel.DEBUG });
|
|
expect(logs).toHaveLength(2);
|
|
});
|
|
|
|
it('filtra por nivel mínimo', () => {
|
|
const logr = createLogr(ling, { level: LogLevel.DEBUG });
|
|
|
|
logr.debug('cat1', 'msg1');
|
|
logr.info('cat2', 'msg2');
|
|
logr.warn('cat1', 'msg3');
|
|
logr.error('cat2', 'msg4');
|
|
|
|
const logs = logr.getLogs({ minLevel: LogLevel.WARN });
|
|
expect(logs).toHaveLength(2);
|
|
expect(logs.every(l => l.level >= LogLevel.WARN)).toBe(true);
|
|
});
|
|
|
|
it('filtra por categoría', () => {
|
|
const logr = createLogr(ling, { level: LogLevel.DEBUG });
|
|
|
|
logr.debug('auth', 'msg1');
|
|
logr.info('db', 'msg2');
|
|
logr.warn('auth', 'msg3');
|
|
|
|
const logs = logr.getLogs({ category: 'auth' });
|
|
expect(logs).toHaveLength(2);
|
|
expect(logs.every(l => l.category === 'auth')).toBe(true);
|
|
});
|
|
|
|
it('filtra por fecha (since)', async () => {
|
|
const logr = createLogr(ling, { level: LogLevel.DEBUG });
|
|
|
|
logr.debug('cat1', 'msg1');
|
|
// Pequeño delay para asegurar diferencia en timestamps
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
const now = new Date();
|
|
logr.info('cat2', 'msg2');
|
|
|
|
const logs = logr.getLogs({ since: now });
|
|
expect(logs).toHaveLength(1);
|
|
expect(logs[0].message).toBe('msg2');
|
|
});
|
|
|
|
it('combina múltiples filtros con AND', () => {
|
|
const logr = createLogr(ling, { level: LogLevel.DEBUG });
|
|
|
|
logr.debug('auth', 'msg1');
|
|
logr.info('db', 'msg2');
|
|
logr.warn('auth', 'msg3');
|
|
logr.error('auth', 'msg4');
|
|
|
|
const logs = logr.getLogs({ category: 'auth', minLevel: LogLevel.WARN });
|
|
expect(logs).toHaveLength(2);
|
|
expect(logs.every(l => l.category === 'auth' && l.level >= LogLevel.WARN)).toBe(true);
|
|
});
|
|
|
|
it('devuelve una copia defensiva — mutar no afecta el historial interno', () => {
|
|
const logr = createLogr(ling, { level: LogLevel.DEBUG });
|
|
|
|
logr.info('test', 'original');
|
|
|
|
const logs = logr.getLogs();
|
|
logs[0].message = 'mutated';
|
|
|
|
const logs2 = logr.getLogs();
|
|
expect(logs2[0].message).toBe('original');
|
|
});
|
|
});
|
|
|
|
describe('createLogr — clear()', () => {
|
|
it('vacía el historial', () => {
|
|
const logr = createLogr(ling, { level: LogLevel.DEBUG });
|
|
|
|
logr.debug('test', 'msg1');
|
|
logr.info('test', 'msg2');
|
|
expect(logr.getLogs()).toHaveLength(2);
|
|
|
|
logr.clear();
|
|
expect(logr.getLogs()).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// ENGINE — serialize
|
|
// ============================================================================
|
|
|
|
describe('createLogr — serialize()', () => {
|
|
it('exporta el historial como JSON', () => {
|
|
const logr = createLogr(ling, { level: LogLevel.DEBUG });
|
|
|
|
logr.info('auth', { es: 'Inicio de sesión', en: 'Login' });
|
|
logr.error('db', 'Connection failed');
|
|
|
|
const json = logr.serialize();
|
|
const parsed = JSON.parse(json);
|
|
|
|
expect(parsed).toHaveLength(2);
|
|
expect(parsed[0].category).toBe('auth');
|
|
expect(parsed[0].message).toBe('Inicio de sesión');
|
|
expect(parsed[1].category).toBe('db');
|
|
expect(parsed[1].message).toBe('Connection failed');
|
|
});
|
|
|
|
it('serializa el timestamp como ISO string', () => {
|
|
const logr = createLogr(ling, { level: LogLevel.DEBUG });
|
|
|
|
logr.info('test', 'msg');
|
|
|
|
const json = logr.serialize();
|
|
const parsed = JSON.parse(json);
|
|
|
|
expect(parsed[0].timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
});
|
|
|
|
it('preserva el locale original del mensaje', () => {
|
|
const logr = createLogr(ling, { level: LogLevel.DEBUG });
|
|
|
|
logr.info('test', { es: 'Mensaje en español', en: 'English message' });
|
|
|
|
ling.setLocale('en');
|
|
const json = logr.serialize();
|
|
const parsed = JSON.parse(json);
|
|
|
|
// El mensaje serializado debe ser el del locale original (es)
|
|
expect(parsed[0].message).toBe('Mensaje en español');
|
|
});
|
|
|
|
it('serializa el contexto si existe', () => {
|
|
const logr = createLogr(ling, { level: LogLevel.DEBUG });
|
|
|
|
logr.info('test', 'msg', { userId: 123, extra: 'data' });
|
|
|
|
const json = logr.serialize();
|
|
const parsed = JSON.parse(json);
|
|
|
|
expect(parsed[0].context).toEqual({ userId: 123, extra: 'data' });
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// ENGINE — configuración en runtime
|
|
// ============================================================================
|
|
|
|
describe('createLogr — setLevel()', () => {
|
|
let captured: string[];
|
|
|
|
beforeEach(() => {
|
|
captured = [];
|
|
});
|
|
|
|
it('cambia el nivel mínimo en runtime', () => {
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.ERROR,
|
|
transports: [callbackTransport((_, msg) => captured.push(msg))],
|
|
});
|
|
|
|
logr.debug('test', 'd');
|
|
logr.info('test', 'i');
|
|
expect(captured).toHaveLength(0);
|
|
|
|
logr.setLevel(LogLevel.DEBUG);
|
|
logr.debug('test', 'd');
|
|
logr.info('test', 'i');
|
|
|
|
expect(captured).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe('createLogr — setMaxLogs()', () => {
|
|
it('limita el tamaño del historial', () => {
|
|
const logr = createLogr(ling, { level: LogLevel.DEBUG, maxLogs: 3 });
|
|
|
|
logr.debug('test', '1');
|
|
logr.debug('test', '2');
|
|
logr.debug('test', '3');
|
|
logr.debug('test', '4');
|
|
|
|
const logs = logr.getLogs();
|
|
expect(logs).toHaveLength(3);
|
|
expect(logs[0].message).toBe('2'); // el más antiguo fue descartado
|
|
expect(logs[2].message).toBe('4');
|
|
});
|
|
|
|
it('recorta el historial si se reduce el maxLogs', () => {
|
|
const logr = createLogr(ling, { level: LogLevel.DEBUG, maxLogs: 100 });
|
|
|
|
logr.debug('test', '1');
|
|
logr.debug('test', '2');
|
|
logr.debug('test', '3');
|
|
logr.debug('test', '4');
|
|
|
|
expect(logr.getLogs()).toHaveLength(4);
|
|
|
|
logr.setMaxLogs(2);
|
|
|
|
const logs = logr.getLogs();
|
|
expect(logs).toHaveLength(2);
|
|
expect(logs[0].message).toBe('3');
|
|
expect(logs[1].message).toBe('4');
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// TRANSPORTS
|
|
// ============================================================================
|
|
|
|
describe('consoleTransport', () => {
|
|
it('llama al método de consola correcto según el nivel', () => {
|
|
const debugSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
|
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
const transport = consoleTransport();
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.DEBUG,
|
|
transports: [transport],
|
|
});
|
|
|
|
logr.debug('test', 'debug msg');
|
|
logr.info('test', 'info msg');
|
|
logr.warn('test', 'warn msg');
|
|
logr.error('test', 'error msg');
|
|
|
|
expect(debugSpy).toHaveBeenCalled();
|
|
expect(infoSpy).toHaveBeenCalled();
|
|
expect(warnSpy).toHaveBeenCalled();
|
|
expect(errorSpy).toHaveBeenCalled();
|
|
|
|
debugSpy.mockRestore();
|
|
infoSpy.mockRestore();
|
|
warnSpy.mockRestore();
|
|
errorSpy.mockRestore();
|
|
});
|
|
|
|
it('puede desactivar timestamp', () => {
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
|
|
const transport = consoleTransport({ timestamp: false });
|
|
transport.write({
|
|
timestamp: new Date(),
|
|
level: LogLevel.WARN,
|
|
category: 'test',
|
|
message: 'msg',
|
|
locale: 'es',
|
|
}, 'resolved');
|
|
|
|
const firstArg = warnSpy.mock.calls[0][0];
|
|
expect(firstArg).not.toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
|
|
warnSpy.mockRestore();
|
|
});
|
|
|
|
it('puede desactivar el prefijo de categoría', () => {
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
|
|
const transport = consoleTransport({ prefix: false });
|
|
transport.write({
|
|
timestamp: new Date(),
|
|
level: LogLevel.WARN,
|
|
category: 'test',
|
|
message: 'msg',
|
|
locale: 'es',
|
|
}, 'resolved');
|
|
|
|
const hasCategoryPrefix = warnSpy.mock.calls[0].some((arg: unknown) =>
|
|
typeof arg === 'string' && arg === '[test]'
|
|
);
|
|
expect(hasCategoryPrefix).toBe(false);
|
|
|
|
warnSpy.mockRestore();
|
|
});
|
|
|
|
it('incluye el contexto si existe', () => {
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
|
|
const transport = consoleTransport();
|
|
transport.write({
|
|
timestamp: new Date(),
|
|
level: LogLevel.WARN,
|
|
category: 'test',
|
|
message: 'msg',
|
|
locale: 'es',
|
|
context: { key: 'value' },
|
|
}, 'resolved');
|
|
|
|
const callArgs = warnSpy.mock.calls[0];
|
|
expect(callArgs).toContainEqual({ key: 'value' });
|
|
|
|
warnSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('httpTransport', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('envía POST al endpoint especificado', async () => {
|
|
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue(new Response());
|
|
|
|
const transport = httpTransport({ url: 'https://logs.example.com/api' });
|
|
transport.write({
|
|
timestamp: new Date(),
|
|
level: LogLevel.ERROR,
|
|
category: 'test',
|
|
message: 'error msg',
|
|
locale: 'es',
|
|
}, 'resolved');
|
|
|
|
expect(fetchSpy).toHaveBeenCalledWith(
|
|
'https://logs.example.com/api',
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
headers: expect.objectContaining({
|
|
'Content-Type': 'application/json',
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('incluye headers personalizados', async () => {
|
|
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue(new Response());
|
|
|
|
const transport = httpTransport({
|
|
url: 'https://logs.example.com/api',
|
|
headers: { 'Authorization': 'Bearer token123' },
|
|
});
|
|
transport.write({
|
|
timestamp: new Date(),
|
|
level: LogLevel.ERROR,
|
|
category: 'test',
|
|
message: 'msg',
|
|
locale: 'es',
|
|
}, 'resolved');
|
|
|
|
expect(fetchSpy).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
'Authorization': 'Bearer token123',
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('filtra por nivel mínimo (por defecto ERROR)', async () => {
|
|
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue(new Response());
|
|
|
|
const transport = httpTransport({ url: 'https://logs.example.com/api' });
|
|
|
|
transport.write({
|
|
timestamp: new Date(),
|
|
level: LogLevel.WARN,
|
|
category: 'test',
|
|
message: 'warn',
|
|
locale: 'es',
|
|
}, 'resolved');
|
|
|
|
transport.write({
|
|
timestamp: new Date(),
|
|
level: LogLevel.ERROR,
|
|
category: 'test',
|
|
message: 'error',
|
|
locale: 'es',
|
|
}, 'resolved');
|
|
|
|
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('puede configurar un nivel mínimo diferente', async () => {
|
|
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue(new Response());
|
|
|
|
const transport = httpTransport({
|
|
url: 'https://logs.example.com/api',
|
|
level: LogLevel.WARN,
|
|
});
|
|
|
|
transport.write({
|
|
timestamp: new Date(),
|
|
level: LogLevel.WARN,
|
|
category: 'test',
|
|
message: 'warn',
|
|
locale: 'es',
|
|
}, 'resolved');
|
|
|
|
transport.write({
|
|
timestamp: new Date(),
|
|
level: LogLevel.ERROR,
|
|
category: 'test',
|
|
message: 'error',
|
|
locale: 'es',
|
|
}, 'resolved');
|
|
|
|
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('loggea en consola si fetch falla (fire-and-forget)', async () => {
|
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
vi.spyOn(global, 'fetch').mockRejectedValue(new Error('Network error'));
|
|
|
|
const transport = httpTransport({ url: 'https://logs.example.com/api' });
|
|
transport.write({
|
|
timestamp: new Date(),
|
|
level: LogLevel.ERROR,
|
|
category: 'test',
|
|
message: 'error',
|
|
locale: 'es',
|
|
}, 'resolved');
|
|
|
|
// Esperar a que la promesa sea rechazada
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
'[logr:httpTransport] Failed to send log:',
|
|
expect.any(Error)
|
|
);
|
|
|
|
consoleErrorSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('callbackTransport', () => {
|
|
it('invoca el callback con cada entrada', () => {
|
|
const received: Array<{ entry: any; message: string }> = [];
|
|
|
|
const transport = callbackTransport((entry, msg) => {
|
|
received.push({ entry, message: msg });
|
|
});
|
|
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.DEBUG,
|
|
transports: [transport],
|
|
});
|
|
|
|
logr.info('test', 'callback test');
|
|
|
|
expect(received).toHaveLength(1);
|
|
expect(received[0].message).toBe('callback test');
|
|
expect(received[0].entry.category).toBe('test');
|
|
});
|
|
|
|
it('permite capturar logs para tests', () => {
|
|
const captured: string[] = [];
|
|
|
|
const transport = callbackTransport((_, msg) => captured.push(msg));
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.DEBUG,
|
|
transports: [transport],
|
|
});
|
|
|
|
logr.debug('test', 'one');
|
|
logr.info('test', 'two');
|
|
logr.warn('test', 'three');
|
|
|
|
expect(captured).toEqual(['one', 'two', 'three']);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// MÚLTIPLES TRANSPORTS
|
|
// ============================================================================
|
|
|
|
describe('múltiples transports', () => {
|
|
it('emite a todos los transports registrados', () => {
|
|
const consoleCaptured: string[] = [];
|
|
const customCaptured: string[] = [];
|
|
|
|
const consoleTrans = callbackTransport((_, msg) => consoleCaptured.push(msg));
|
|
const customTrans = callbackTransport((_, msg) => customCaptured.push(msg));
|
|
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.INFO,
|
|
transports: [consoleTrans, customTrans],
|
|
});
|
|
|
|
logr.info('test', 'multi transport');
|
|
|
|
expect(consoleCaptured).toHaveLength(1);
|
|
expect(customCaptured).toHaveLength(1);
|
|
expect(consoleCaptured[0]).toBe('multi transport');
|
|
expect(customCaptured[0]).toBe('multi transport');
|
|
});
|
|
|
|
it('un transport que falla no impide que los demás reciban la entrada', () => {
|
|
const goodCaptured: string[] = [];
|
|
|
|
const failingTrans = callbackTransport(() => {
|
|
throw new Error('Transport error');
|
|
});
|
|
const goodTrans = callbackTransport((_, msg) => goodCaptured.push(msg));
|
|
|
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
const logr = createLogr(ling, {
|
|
level: LogLevel.INFO,
|
|
transports: [failingTrans, goodTrans],
|
|
});
|
|
|
|
logr.info('test', 'msg');
|
|
|
|
expect(goodCaptured).toHaveLength(1);
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
'[logr] Transport error:',
|
|
expect.any(Error)
|
|
);
|
|
|
|
consoleErrorSpy.mockRestore();
|
|
});
|
|
});
|