From 600bb46b09cf35fe2eb0056737487f6ff2e7d1f7 Mon Sep 17 00:00:00 2001 From: borja Date: Sat, 2 May 2026 01:06:01 +0200 Subject: [PATCH] =?UTF-8?q?resuelve=20un=20problema=20que=20hac=C3=ADa=20q?= =?UTF-8?q?ue=20no=20se=20pudiesen=20soltar=20tareas=20para=20algunos=20us?= =?UTF-8?q?uarios=20y=20crea=20tests=20que=20vigilan=20esa=20posible=20reg?= =?UTF-8?q?resi=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/http/webhook-handler.ts | 12 +- .../server.sender-canonicalization.test.ts | 280 ++++++++++++++++++ 2 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 tests/unit/server.sender-canonicalization.test.ts diff --git a/src/http/webhook-handler.ts b/src/http/webhook-handler.ts index 0b380b4..d4b0310 100644 --- a/src/http/webhook-handler.ts +++ b/src/http/webhook-handler.ts @@ -396,8 +396,18 @@ export async function handleMessageUpsert(data: any, db: Database): Promise ensureUserExists(uid, testDb)!) + .filter(Boolean); + + const taskId = TaskService.createTask( + { + description: opts.description, + due_date: opts.dueDate ?? '2026-12-31', + group_id: opts.groupId || null, + created_by: creator, + }, + assigneeIds.map(uid => ({ user_id: uid, assigned_by: creator })), + ); + + const row = testDb + .prepare('SELECT display_code FROM tasks WHERE id = ?') + .get(taskId) as { display_code: number } | undefined; + const dc = row?.display_code ?? 1; + + return { id: taskId, displayCode: dc }; +} + +/** Code-pad a display_code for use in command messages. */ +function code4(n: number): string { + return String(n).padStart(4, '0'); +} + +/** Seed a LID → real-number alias in the identity system. */ +function seedAlias(lidId: string, realNumber: string): void { + IdentityService.upsertAlias(lidId, `${realNumber}@s.whatsapp.net`, 'test-seed'); +} + +// ── Tests ────────────────────────────────────────────────────────────── + +describe('Sender canonicalization via alias table', () => { + test('soltar succeeds when sender is LID-based and assignment uses real number', async () => { + // 1) Set up alias: LID → real number + seedAlias('userXYZ@lid', '666777888'); + + // 2) Create a task assigned to the real number, with a group_id + // so soltar actually unassigns (single-assignee + no-group = forbidden) + const { displayCode } = createTaskWithAssignees({ + description: 'Tarea de prueba', + creator: '2222222222', + assignees: ['666777888'], + groupId: 'group-id@g.us', + }); + + // 3) User sends t soltar from a group, with ONLY participant (LID), + // NO participantAlt — simulating the fallback path that triggers + // the canonicalization code. + const payload = { + event: 'messages.upsert', + instance: 'test-instance', + data: { + key: { + remoteJid: 'group-id@g.us', + participant: 'userXYZ@lid', + // intentionally omit participantAlt + }, + message: { conversation: `t soltar ${displayCode}` }, + }, + }; + + const response = await WebhookServer.handleRequest(createTestRequest(payload)); + expect(response.status).toBe(200); + + const out = SimulatedResponseQueue.get(); + const msg = out.map(r => r.message).join('\n'); + + // Without canonicalization, the sender would be 'userXYZ' (LID) and + // soltar would fail with "no la tenías asignada" because the assignment + // uses '666777888' (real number). With canonicalization, the sender is + // resolved to '666777888' and soltar succeeds. + expect(msg).toContain('queda sin responsable'); + }); + + test('soltar via DM with real sender works end-to-end', async () => { + // 1) Create alias and task + seedAlias('userXYZ@lid', '666777888'); + const { displayCode } = createTaskWithAssignees({ + description: 'Tarea DM test', + creator: '2222222222', + assignees: ['666777888'], + }); + + // 2) Send from DM — remoteJid is the user's JID, no LID ambiguity + const payload = { + event: 'messages.upsert', + instance: 'test-instance', + data: { + key: { + remoteJid: '666777888@s.whatsapp.net', + }, + message: { conversation: `t soltar ${displayCode}` }, + }, + }; + + const response = await WebhookServer.handleRequest(createTestRequest(payload)); + expect(response.status).toBe(200); + + const out = SimulatedResponseQueue.get(); + const msg = out.map(r => r.message).join('\n'); + + // The soltar should succeed — task is personal (single assignee, no group) + // so it shows "No puedes soltar una tarea personal" + // OR if created with a group_id, it would show the unassign success message + expect(msg).toMatch(/No puedes soltar|Has soltado|queda sin responsable/); + }); + + test('canonicalization does not alter already-canonical sender', async () => { + // When participantAlt (real number) is present, resolveSenderJid uses it + // directly. The canonicalization resolves the alias table but finds no + // mapping (real→real is the identity), so it's a no-op. + const { displayCode } = createTaskWithAssignees({ + description: 'Already canonical', + creator: '2222222222', + assignees: ['666777888'], + groupId: 'group-id@g.us', + }); + + const payload = { + event: 'messages.upsert', + instance: 'test-instance', + data: { + key: { + remoteJid: 'group-id@g.us', + participant: 'userXYZ@lid', + participantAlt: '666777888@s.whatsapp.net', + }, + message: { conversation: `t soltar ${displayCode}` }, + }, + }; + + const response = await WebhookServer.handleRequest(createTestRequest(payload)); + expect(response.status).toBe(200); + + const out = SimulatedResponseQueue.get(); + const msg = out.map(r => r.message).join('\n'); + + // Soltar succeeds because participantAlt provides the canonical real number. + // The key thing: it didn't crash or produce a wrong-identity error. + expect(msg).toContain('queda sin responsable'); + }); + + test('soltar without alias mapping falls through to raw sender', async () => { + // No alias seeded — sender stays as-is (whatever normalizeWhatsAppId produces). + // This tests the non-regression path: the canonicalization gracefully + // returns null and the raw sender is used unchanged. + const { displayCode } = createTaskWithAssignees({ + description: 'No alias task', + creator: '2222222222', + assignees: ['666777888'], + groupId: 'group-id@g.us', + }); + + // A different user (LID 'otherLID', no alias) tries to soltar. + // Without alias mapping, sender stays as 'otherLID'. + const payload = { + event: 'messages.upsert', + instance: 'test-instance', + data: { + key: { + remoteJid: 'group-id@g.us', + participant: 'otherLID@lid', + // no participantAlt + }, + message: { conversation: `t soltar ${displayCode}` }, + }, + }; + + const response = await WebhookServer.handleRequest(createTestRequest(payload)); + expect(response.status).toBe(200); + + const out = SimulatedResponseQueue.get(); + const msg = out.map(r => r.message).join('\n'); + + // "no la tenías asignada" because 'otherLID' != '666777888'. + // This confirms the canonicalization gracefully handles missing aliases. + expect(msg).toContain('no la tenías asignada'); + }); + + test('t todas resolves unassigned tasks with canonicalized sender', async () => { + // 1) Seed alias LID → real number + seedAlias('userXYZ@lid', '666777888'); + + // 2) Add user as active member of a group + testDb.exec(` + INSERT OR IGNORE INTO groups (id, name, active, community_id) + VALUES ('another-group@g.us', 'Another Group', 1, '') + `); + ensureUserExists('666777888', testDb); + testDb.exec(` + INSERT OR IGNORE INTO group_members (group_id, user_id, is_active, is_admin) + VALUES ('another-group@g.us', '666777888', 1, 0) + `); + + // 3) Refresh the active groups cache + (GroupSyncService as any).cacheActiveGroups(); + + // 4) Create two unassigned tasks in that group + TaskService.createTask( + { description: 'Sin dueño A', due_date: '2026-12-31', group_id: 'another-group@g.us', created_by: '2222222222' }, + [], + ); + TaskService.createTask( + { description: 'Sin dueño B', due_date: '2026-12-31', group_id: 'another-group@g.us', created_by: '2222222222' }, + [], + ); + + // 5) Also create a task assigned to the user + createTaskWithAssignees({ + description: 'Mi tarea', + creator: '2222222222', + assignees: ['666777888'], + groupId: 'another-group@g.us', + }); + + // 6) Send t todas from DM with canonical (real) sender + const payload = { + event: 'messages.upsert', + instance: 'test-instance', + data: { + key: { + remoteJid: '666777888@s.whatsapp.net', + }, + message: { conversation: 't todas' }, + }, + }; + + const response = await WebhookServer.handleRequest(createTestRequest(payload)); + expect(response.status).toBe(200); + + const out = SimulatedResponseQueue.get(); + const msg = out.map(r => r.message).join('\n'); + + // Should show "Tus tareas" section with the assigned task + expect(msg).toContain('Tus tareas'); + expect(msg).toContain('Mi tarea'); + + // Should show the "Sin responsable" section for the other group + expect(msg).toContain('Sin responsable'); + expect(msg).toContain('Sin dueño A'); + expect(msg).toContain('Sin dueño B'); + }); +});