diff --git a/tests/unit/tasks/complete-reaction.test.ts b/tests/unit/tasks/complete-reaction.test.ts new file mode 100644 index 0000000..ecc293d --- /dev/null +++ b/tests/unit/tasks/complete-reaction.test.ts @@ -0,0 +1,158 @@ +import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'bun:test'; +import { Database } from 'bun:sqlite'; +import { initializeDatabase } from '../../../src/db'; +import { TaskService } from '../../../src/tasks/service'; +import { ResponseQueue } from '../../../src/services/response-queue'; +import { AllowedGroups } from '../../../src/services/allowed-groups'; + +function toIsoSql(d: Date): string { + return d.toISOString().replace('T', ' ').replace('Z', ''); +} + +describe('TaskService - reacción ✅ al completar (Fase 2)', () => { + let memdb: Database; + let envBackup: Record; + + beforeAll(() => { + envBackup = { ...process.env }; + memdb = new Database(':memory:'); + initializeDatabase(memdb); + (TaskService as any).dbInstance = memdb; + (ResponseQueue as any).dbInstance = memdb; + (AllowedGroups as any).dbInstance = memdb; + }); + + afterAll(() => { + process.env = envBackup; + try { memdb.close(); } catch {} + }); + + beforeEach(() => { + process.env.NODE_ENV = 'test'; + process.env.REACTIONS_ENABLED = 'true'; + process.env.REACTIONS_SCOPE = 'groups'; + process.env.REACTIONS_TTL_DAYS = '14'; + process.env.GROUP_GATING_MODE = 'enforce'; + + memdb.exec(` + DELETE FROM response_queue; + DELETE FROM task_assignments; + DELETE FROM tasks; + DELETE FROM users; + DELETE FROM task_origins; + DELETE FROM allowed_groups; + `); + }); + + it('enqueuea ✅ al completar una tarea con task_origins dentro de TTL y grupo allowed', async () => { + const groupId = 'grp-1@g.us'; + AllowedGroups.setStatus(groupId, 'allowed'); + + const taskId = TaskService.createTask({ + description: 'Prueba ✅', + due_date: null, + group_id: groupId, + created_by: '600111222' + }); + + // Origen reciente (dentro de TTL) + const msgId = 'MSG-OK-1'; + memdb.prepare(` + INSERT INTO task_origins (task_id, chat_id, message_id, created_at) + VALUES (?, ?, ?, ?) + `).run(taskId, groupId, msgId, toIsoSql(new Date())); + + const res = TaskService.completeTask(taskId, '600111222'); + expect(res.status).toBe('updated'); + + const row = memdb.prepare(`SELECT id, recipient, metadata FROM response_queue ORDER BY id DESC LIMIT 1`).get() as any; + expect(row).toBeTruthy(); + expect(String(row.recipient)).toBe(groupId); + + const meta = JSON.parse(String(row.metadata || '{}')); + expect(meta.kind).toBe('reaction'); + expect(meta.emoji).toBe('✅'); + expect(meta.chatId).toBe(groupId); + expect(meta.messageId).toBe(msgId); + }); + + it('no encola ✅ si el origen está fuera de TTL', async () => { + const groupId = 'grp-2@g.us'; + AllowedGroups.setStatus(groupId, 'allowed'); + + // TTL 7 días para forzar expiración + process.env.REACTIONS_TTL_DAYS = '7'; + + const taskId = TaskService.createTask({ + description: 'Fuera TTL', + due_date: null, + group_id: groupId, + created_by: '600111222' + }); + + const msgId = 'MSG-OLD-1'; + const old = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000); // 8 días atrás + memdb.prepare(` + INSERT INTO task_origins (task_id, chat_id, message_id, created_at) + VALUES (?, ?, ?, ?) + `).run(taskId, groupId, msgId, toIsoSql(old)); + + const res = TaskService.completeTask(taskId, '600111222'); + expect(res.status).toBe('updated'); + + const cnt = memdb.prepare(`SELECT COUNT(*) AS c FROM response_queue`).get() as any; + expect(Number(cnt.c)).toBe(0); + }); + + it('idempotencia: completar dos veces encola solo un ✅', async () => { + const groupId = 'grp-3@g.us'; + AllowedGroups.setStatus(groupId, 'allowed'); + + const taskId = TaskService.createTask({ + description: 'Idempotencia ✅', + due_date: null, + group_id: groupId, + created_by: '600111222' + }); + + const msgId = 'MSG-IDEMP-1'; + memdb.prepare(` + INSERT INTO task_origins (task_id, chat_id, message_id, created_at) + VALUES (?, ?, ?, ?) + `).run(taskId, groupId, msgId, toIsoSql(new Date())); + + const r1 = TaskService.completeTask(taskId, '600111222'); + const r2 = TaskService.completeTask(taskId, '600111222'); + expect(r1.status === 'updated' || r1.status === 'already').toBe(true); + expect(r2.status === 'updated' || r2.status === 'already').toBe(true); + + const rows = memdb.query(`SELECT metadata FROM response_queue`).all() as any[]; + expect(rows.length).toBe(1); + const meta = JSON.parse(String(rows[0].metadata || '{}')); + expect(meta.emoji).toBe('✅'); + }); + + it('enforce: grupo no allowed → no encola ✅', async () => { + const groupId = 'grp-4@g.us'; + // Estado por defecto 'pending' (no allowed) + + const taskId = TaskService.createTask({ + description: 'No allowed', + due_date: null, + group_id: groupId, + created_by: '600111222' + }); + + const msgId = 'MSG-NO-ALLOW-1'; + memdb.prepare(` + INSERT INTO task_origins (task_id, chat_id, message_id, created_at) + VALUES (?, ?, ?, ?) + `).run(taskId, groupId, msgId, toIsoSql(new Date())); + + const res = TaskService.completeTask(taskId, '600111222'); + expect(res.status === 'updated' || res.status === 'already').toBe(true); + + const cnt = memdb.prepare(`SELECT COUNT(*) AS c FROM response_queue`).get() as any; + expect(Number(cnt.c)).toBe(0); + }); +});