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.
svelte-kit-vice/src/lib/logr/test/logr.test.ts

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();
});
});

Powered by TurnKey Linux.