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