import { beforeEach, afterEach, describe, expect, it } from 'bun:test'; import { createTempDb } from './helpers/db'; // Los imports del handler y closeDb se hacen dinámicos dentro de cada test/teardown function toIsoSql(d: Date): string { return d.toISOString().replace('T', ' ').replace('Z', ''); } describe('Web API - completar tarea encola reacción ✅', () => { let cleanup: () => void; let db: any; let path: string; const USER = '34600123456'; const GROUP_ID = '12345-67890@g.us'; beforeEach(() => { const tmp = createTempDb(); cleanup = tmp.cleanup; db = tmp.db; path = tmp.path; process.env.NODE_ENV = 'test'; process.env.DB_PATH = path; process.env.REACTIONS_ENABLED = 'true'; process.env.REACTIONS_SCOPE = 'groups'; process.env.REACTIONS_TTL_DAYS = '14'; process.env.GROUP_GATING_MODE = 'enforce'; // Sembrar usuario y grupo permitido + membresía activa db.prepare(`INSERT OR IGNORE INTO users (id) VALUES (?)`).run(USER); db.prepare(`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES (?, 'comm-1', 'Group', 1)`).run(GROUP_ID); db.prepare(`INSERT OR REPLACE INTO allowed_groups (group_id, label, status) VALUES (?, 'Test', 'allowed')`).run(GROUP_ID); db.prepare(`INSERT OR REPLACE INTO group_members (group_id, user_id, is_admin, is_active) VALUES (?, ?, 0, 1)`).run(GROUP_ID, USER); }); afterEach(async () => { // Cerrar la conexión singleton de la web antes de borrar el archivo try { const { closeDb } = await import('../../apps/web/src/lib/server/db.ts'); closeDb(); } catch {} if (cleanup) cleanup(); // Limpiar env relevantes delete process.env.DB_PATH; delete process.env.REACTIONS_ENABLED; delete process.env.REACTIONS_SCOPE; delete process.env.REACTIONS_TTL_DAYS; delete process.env.GROUP_GATING_MODE; }); it('caso feliz: encola 1 reacción ✅ con metadata canónica', async () => { // Crear tarea en grupo (no completada) const ins = db.prepare(` INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at) VALUES ('Probar reacción', NULL, ?, ?, 0, NULL) `).run(GROUP_ID, USER) as any; const taskId = Number(ins.lastInsertRowid); // Origen reciente con participant y from_me=1 const messageId = 'MSG-abc-123'; db.prepare(` INSERT OR REPLACE INTO task_origins (task_id, chat_id, message_id, created_at, participant, from_me) VALUES (?, ?, ?, ?, ?, ?) `).run(taskId, GROUP_ID, messageId, toIsoSql(new Date()), `${USER}@s.whatsapp.net`, 1); // Ejecutar endpoint const event: any = { locals: { userId: USER }, params: { id: String(taskId) }, request: new Request('http://localhost', { method: 'POST' }) }; const { POST: completeHandler } = await import('../../apps/web/src/routes/api/tasks/[id]/complete/+server.ts'); const res = await completeHandler(event); expect(res.status).toBe(200); const payload = await res.json(); expect(payload.status).toBe('updated'); // Verificar encolado const row = db.prepare(`SELECT recipient, message, metadata FROM response_queue ORDER BY id DESC LIMIT 1`).get() as any; expect(row).toBeTruthy(); expect(String(row.recipient)).toBe(GROUP_ID); expect(String(row.message)).toBe(''); const meta = JSON.parse(String(row.metadata || '{}')); expect(meta).toEqual({ kind: 'reaction', emoji: '✅', chatId: GROUP_ID, messageId, fromMe: true, participant: `${USER}@s.whatsapp.net` }); // Idempotencia del endpoint: segunda llamada no crea nuevo job const res2 = await completeHandler(event); expect(res2.status).toBe(200); const body2 = await res2.json(); expect(body2.status).toBe('already'); const cnt = db.prepare(`SELECT COUNT(*) AS c FROM response_queue WHERE metadata = ?`).get(JSON.stringify(meta)) as any; expect(Number(cnt.c || 0)).toBe(1); }); it('TTL vencido: no encola reacción', async () => { const ins = db.prepare(` INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at) VALUES ('Vieja', NULL, ?, ?, 0, NULL) `).run(GROUP_ID, USER) as any; const taskId = Number(ins.lastInsertRowid); const messageId = 'MSG-old-001'; const old = new Date(Date.now() - 20 * 24 * 60 * 60 * 1000); // 20 días db.prepare(` INSERT OR REPLACE INTO task_origins (task_id, chat_id, message_id, created_at) VALUES (?, ?, ?, ?) `).run(taskId, GROUP_ID, messageId, toIsoSql(old)); const event: any = { locals: { userId: USER }, params: { id: String(taskId) }, request: new Request('http://localhost', { method: 'POST' }) }; const { POST: completeHandler } = await import('../../apps/web/src/routes/api/tasks/[id]/complete/+server.ts'); const res = await completeHandler(event); expect(res.status).toBe(200); const body = await res.json(); expect(body.status).toBe('updated'); const cnt = db.prepare(`SELECT COUNT(*) AS c FROM response_queue`).get() as any; expect(Number(cnt.c || 0)).toBe(0); }); it('scope=groups: origen DM no encola', async () => { const ins = db.prepare(` INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at) VALUES ('DM scope', NULL, ?, ?, 0, NULL) `).run(GROUP_ID, USER) as any; const taskId = Number(ins.lastInsertRowid); const messageId = 'MSG-dm-001'; const dmChat = `${USER}@s.whatsapp.net`; // no @g.us db.prepare(` INSERT OR REPLACE INTO task_origins (task_id, chat_id, message_id, created_at) VALUES (?, ?, ?, ?) `).run(taskId, dmChat, messageId, toIsoSql(new Date())); const event: any = { locals: { userId: USER }, params: { id: String(taskId) }, request: new Request('http://localhost', { method: 'POST' }) }; const { POST: completeHandler } = await import('../../apps/web/src/routes/api/tasks/[id]/complete/+server.ts'); const res = await completeHandler(event); expect(res.status).toBe(200); const cnt = db.prepare(`SELECT COUNT(*) AS c FROM response_queue`).get() as any; expect(Number(cnt.c || 0)).toBe(0); }); it('sin participant/from_me: metadata no incluye claves opcionales', async () => { const ins = db.prepare(` INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at) VALUES ('Sin opcionales', NULL, ?, ?, 0, NULL) `).run(GROUP_ID, USER) as any; const taskId = Number(ins.lastInsertRowid); const messageId = 'MSG-nopts-001'; db.prepare(` INSERT OR REPLACE INTO task_origins (task_id, chat_id, message_id, created_at) VALUES (?, ?, ?, ?) `).run(taskId, GROUP_ID, messageId, toIsoSql(new Date())); const event: any = { locals: { userId: USER }, params: { id: String(taskId) }, request: new Request('http://localhost', { method: 'POST' }) }; const { POST: completeHandler } = await import('../../apps/web/src/routes/api/tasks/[id]/complete/+server.ts'); const res = await completeHandler(event); expect(res.status).toBe(200); const row = db.prepare(`SELECT metadata FROM response_queue ORDER BY id DESC LIMIT 1`).get() as any; const meta = JSON.parse(String(row.metadata || '{}')); expect(meta).toEqual({ kind: 'reaction', emoji: '✅', chatId: GROUP_ID, messageId }); expect('fromMe' in meta).toBe(false); expect('participant' in meta).toBe(false); }); });