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.
v/src/logr/tests/logr_transports.test.ts

253 lines
9.3 KiB

import { describe, it, expect, vi, beforeEach } from 'vitest';
import {callbackTransport, consoleTransport, createLogr, httpTransport, type LogEntry, LogLevel} from '@/logr';
import { createLing } from '@/ling';
import type { LingNode, SupportedLocale } from '@/ling';
// ============================================================================
// HELPERS
// ============================================================================
const schema = {
auth: {
loginFailed: { es: 'Login fallido', en: 'Login failed' },
}
} satisfies LingNode;
function makeI18n(locale: SupportedLocale = 'es') {
const i18n = createLing(schema, 'es');
i18n.setLocale(locale);
return i18n;
}
function makeEntry(overrides: Partial<LogEntry> = {}): LogEntry {
return {
timestamp: new Date(),
level : LogLevel.WARN,
category : 'auth',
message : { es: 'Login fallido', en: 'Login failed' },
...overrides,
};
}
// ============================================================================
// consoleTransport
// ============================================================================
describe('consoleTransport', () => {
it('usa console.debug para nivel DEBUG', () => {
const spy = vi.spyOn(console, 'debug').mockImplementation(() => {});
const t = consoleTransport();
t.write(makeEntry({ level: LogLevel.DEBUG }), 'msg debug');
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
});
it('usa console.info para nivel INFO', () => {
const spy = vi.spyOn(console, 'info').mockImplementation(() => {});
const t = consoleTransport();
t.write(makeEntry({ level: LogLevel.INFO }), 'msg info');
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
});
it('usa console.warn para nivel WARN', () => {
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const t = consoleTransport();
t.write(makeEntry({ level: LogLevel.WARN }), 'msg warn');
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
});
it('usa console.error para nivel ERROR', () => {
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
const t = consoleTransport();
t.write(makeEntry({ level: LogLevel.ERROR }), 'msg error');
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
});
it('incluye timestamp y prefijo por defecto', () => {
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const t = consoleTransport();
t.write(makeEntry({ category: 'auth' }), 'mensaje');
expect(spy).toHaveBeenCalledWith(
expect.stringMatching(/^\d{4}-\d{2}-\d{2}/), // ISO timestamp
'[auth]',
'mensaje',
''
);
spy.mockRestore();
});
it('omite timestamp si timestamp: false', () => {
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const t = consoleTransport({ timestamp: false });
t.write(makeEntry({ category: 'auth' }), 'mensaje');
expect(spy).toHaveBeenCalledWith('[auth]', 'mensaje', '');
spy.mockRestore();
});
it('omite prefijo si prefix: false', () => {
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const t = consoleTransport({ prefix: false });
t.write(makeEntry(), 'mensaje');
const args = spy.mock.calls[0];
expect(args).not.toContain('[auth]');
spy.mockRestore();
});
it('incluye el contexto en el output', () => {
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const t = consoleTransport();
t.write(makeEntry({ context: { userId: 42 } }), 'msg');
const args = spy.mock.calls[0];
expect(args).toContainEqual(expect.objectContaining({ userId: 42 }));
spy.mockRestore();
});
});
// ============================================================================
// httpTransport
// ============================================================================
describe('httpTransport', () => {
beforeEach(() => {
global.fetch = vi.fn().mockResolvedValue({ ok: true });
});
it('envía un POST al endpoint configurado', async () => {
const t = httpTransport({ url: 'https://logs.example.com', level: LogLevel.ERROR });
t.write(makeEntry({ level: LogLevel.ERROR }), 'Login failed');
expect(fetch).toHaveBeenCalledWith(
'https://logs.example.com',
expect.objectContaining({ method: 'POST' })
);
});
it('incluye el mensaje resuelto en el body', async () => {
const t = httpTransport({ url: 'https://logs.example.com', level: LogLevel.ERROR });
t.write(makeEntry({ level: LogLevel.ERROR }), 'Login failed');
const body = JSON.parse((fetch as any).mock.calls[0][1].body);
expect(body.message).toBe('Login failed');
});
it('incluye headers custom', () => {
const t = httpTransport({
url : 'https://logs.example.com',
headers: { 'Authorization': 'Bearer token' },
level : LogLevel.ERROR
});
t.write(makeEntry({ level: LogLevel.ERROR }), 'msg');
const headers = (fetch as any).mock.calls[0][1].headers;
expect(headers['Authorization']).toBe('Bearer token');
});
it('no envía si el nivel es inferior al mínimo configurado', () => {
const t = httpTransport({ url: 'https://logs.example.com', level: LogLevel.ERROR });
t.write(makeEntry({ level: LogLevel.WARN }), 'msg');
expect(fetch).not.toHaveBeenCalled();
});
it('usa ERROR como nivel mínimo por defecto', () => {
const t = httpTransport({ url: 'https://logs.example.com' });
t.write(makeEntry({ level: LogLevel.WARN }), 'msg');
expect(fetch).not.toHaveBeenCalled();
t.write(makeEntry({ level: LogLevel.ERROR }), 'msg');
expect(fetch).toHaveBeenCalledTimes(1);
});
it('no lanza si fetch falla — fire and forget', () => {
global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
const t = httpTransport({ url: 'https://logs.example.com', level: LogLevel.ERROR });
expect(() => t.write(makeEntry({ level: LogLevel.ERROR }), 'msg')).not.toThrow();
spy.mockRestore();
});
});
// ============================================================================
// callbackTransport
// ============================================================================
describe('callbackTransport', () => {
it('invoca el callback con la entrada y el mensaje resuelto', () => {
const fn = vi.fn();
const t = callbackTransport(fn);
const entry = makeEntry();
t.write(entry, 'Login fallido');
expect(fn).toHaveBeenCalledWith(entry, 'Login fallido');
});
it('se puede usar para capturar logs en tests', () => {
const i18n = makeI18n('en');
const captured: string[] = [];
const logr = createLogr(i18n, {
level : LogLevel.DEBUG,
transports: [ callbackTransport((_, msg) => captured.push(msg)) ]
});
logr.debug('auth', schema.auth.loginFailed);
logr.warn ('auth', 'custom message');
expect(captured).toEqual(['Login failed', 'custom message']);
});
});
// ============================================================================
// Integración — múltiples transports
// ============================================================================
describe('múltiples transports', () => {
it('emite a todos los transports registrados', () => {
const i18n = makeI18n();
const fn1 = vi.fn();
const fn2 = vi.fn();
const logr = createLogr(i18n, {
level : LogLevel.DEBUG,
transports: [ callbackTransport(fn1), callbackTransport(fn2) ]
});
logr.warn('auth', { es: 'aviso' });
expect(fn1).toHaveBeenCalledTimes(1);
expect(fn2).toHaveBeenCalledTimes(1);
});
it('el mensaje se resuelve una sola vez y se comparte entre transports', () => {
const i18n = makeI18n('en');
const messages: string[] = [];
const logr = createLogr(i18n, {
level : LogLevel.DEBUG,
transports: [
callbackTransport((_, msg) => messages.push(msg)),
callbackTransport((_, msg) => messages.push(msg)),
]
});
logr.debug('auth', schema.auth.loginFailed);
expect(messages).toEqual(['Login failed', 'Login failed']);
});
it('un transport que falla no afecta a los demás', () => {
const i18n = makeI18n();
const fn = vi.fn();
const logr = createLogr(i18n, {
level : LogLevel.DEBUG,
transports: [
callbackTransport(() => { throw new Error('transport error'); }),
callbackTransport(fn),
]
});
// El segundo transport sí debería ejecutarse aunque el primero falle
// Este test documenta el comportamiento actual — si se quiere
// aislamiento habría que añadir try/catch en el engine
expect(() => logr.warn('auth', { es: 'msg' })).toThrow();
});
});

Powered by TurnKey Linux.