import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; import { Database } from 'bun:sqlite'; import { initializeDatabase } from '../../../src/db'; import { CommandService } from '../../../src/services/command'; import { sha256Hex } from '../../../src/utils/crypto'; import { Metrics } from '../../../src/services/metrics'; const envBackup = { ...process.env }; let memdb: Database; describe('CommandService - /t web (emisión de token de login)', () => { beforeEach(() => { process.env = { ...envBackup, NODE_ENV: 'test', TZ: 'Europe/Madrid', WEB_BASE_URL: 'https://app.example.test', METRICS_ENABLED: 'true' }; Metrics.reset?.(); memdb = new Database(':memory:'); initializeDatabase(memdb); (CommandService as any).dbInstance = memdb; }); afterEach(() => { process.env = envBackup; try { memdb.close(); } catch {} }); test('DM feliz: devuelve URL con token y persiste hash en web_tokens', async () => { const res = await CommandService.handle({ sender: '34600123456', groupId: '34600123456@s.whatsapp.net', // DM (no @g.us) message: '/t web', mentions: [] }); expect(Array.isArray(res)).toBe(true); expect(res.length).toBe(1); expect(res[0].recipient).toBe('34600123456'); expect(res[0].message).toContain('https://app.example.test'); const m = res[0].message.match(/https?:\/\/\S+/); expect(m).toBeTruthy(); const url = new URL(m![0]); expect(url.pathname).toBe('/login'); const token = url.searchParams.get('token') || ''; expect(token.length).toBeGreaterThan(0); const hash = await sha256Hex(token); const row = memdb.prepare(` SELECT user_id, token_hash, used_at, expires_at FROM web_tokens WHERE user_id = ? AND token_hash = ? `).get('34600123456', hash) as any; expect(row).toBeTruthy(); expect(row.user_id).toBe('34600123456'); expect(row.token_hash).toBe(hash); expect(row.used_at).toBeNull(); // expires_at debe ser en el futuro const now = new Date(); const exp = new Date(String(row.expires_at).replace(' ', 'T') + 'Z'); expect(exp.getTime()).toBeGreaterThan(now.getTime() + 9 * 60 * 1000 - 10 * 1000); // ~>= 9min50s expect(Metrics.get('web_tokens_issued_total')).toBe(1); }); test('En grupo: responde que debe usarse por privado y no inserta token', async () => { const res = await CommandService.handle({ sender: '34600123456', groupId: '123@g.us', message: '/t web', mentions: [] }); expect(res.length).toBe(1); expect(res[0].recipient).toBe('34600123456'); expect(res[0].message.toLowerCase()).toContain('privado'); const cnt = memdb.prepare(`SELECT COUNT(*) AS c FROM web_tokens WHERE user_id = ?`).get('34600123456') as any; expect(Number(cnt.c)).toBe(0); }); test('Sin WEB_BASE_URL: error claro y no inserta token', async () => { delete process.env.WEB_BASE_URL; const res = await CommandService.handle({ sender: '34600123456', groupId: '34600123456@s.whatsapp.net', message: '/t web', mentions: [] }); expect(res.length).toBe(1); expect(res[0].message).toContain('no está configurada'); const cnt = memdb.prepare(`SELECT COUNT(*) AS c FROM web_tokens WHERE user_id = ?`).get('34600123456') as any; expect(Number(cnt.c)).toBe(0); }); test('Token vigente: se invalida y se emite uno nuevo (queda solo 1 activo)', async () => { // Primera emisión { const r1 = await CommandService.handle({ sender: '34600123456', groupId: '34600123456@s.whatsapp.net', message: '/t web', mentions: [] }); expect(r1.length).toBe(1); } // Segunda emisión (debe invalidar el anterior) { const r2 = await CommandService.handle({ sender: '34600123456', groupId: '34600123456@s.whatsapp.net', message: '/t web', mentions: [] }); expect(r2.length).toBe(1); } const counts = memdb.prepare(` SELECT SUM(CASE WHEN used_at IS NULL THEN 1 ELSE 0 END) AS active, COUNT(*) AS total FROM web_tokens WHERE user_id = ? `).get('34600123456') as any; expect(Number(counts.total)).toBeGreaterThanOrEqual(2); expect(Number(counts.active)).toBe(1); }); });