resuelve un problema que hacía que no se pudiesen soltar tareas para algunos usuarios y crea tests que vigilan esa posible regresión

main
borja 1 month ago
parent fcaafc4600
commit 600bb46b09

@ -396,8 +396,18 @@ export async function handleMessageUpsert(data: any, db: Database): Promise<void
const senderRaw = resolveSenderJid(data, remoteJid);
learnAliasFromKey(data, remoteJid);
const normalizedSenderId = normalizeSender(senderRaw, remoteJid, data.key.participant, !!data.key.fromMe);
let normalizedSenderId = normalizeSender(senderRaw, remoteJid, data.key.participant, !!data.key.fromMe);
if (!normalizedSenderId) return;
// Canonicalise: resolve LID / alternative IDs to the real phone number
try {
const canonical = IdentityService.resolveAliasOrNull(normalizedSenderId);
if (canonical && canonical !== normalizedSenderId) {
log(`[canonical] resolved sender ${normalizedSenderId}${canonical}`);
normalizedSenderId = canonical;
}
} catch {}
if (isOwnBotNumber(normalizedSenderId)) return;
// 4. Ensure user exists

@ -0,0 +1,280 @@
/**
* Sender canonicalization regression tests.
*
* Verifies that when a message arrives in a group with only
* participant (LID-based) and no participantAlt, the sender is
* resolved through the alias table to the canonical real
* WhatsApp number.
*
* This prevents "no la tenías asignada" errors when a user
* tries to soltar a task assigned to their real number but
* their sender is resolved as a LID-based ID.
*/
import { describe, test, expect } from 'bun:test';
import { WebhookServer } from '../../src/server';
import { TaskService } from '../../src/tasks/service';
import { IdentityService } from '../../src/services/identity';
import { GroupSyncService } from '../../src/services/group-sync';
import { SimulatedResponseQueue } from '../helpers/queue';
import { createTestRequest, registerServerTestLifecycle } from '../helpers/server-test-harness';
import { ensureUserExists } from '../../src/db';
const testDb = registerServerTestLifecycle();
// ── Helpers ────────────────────────────────────────────────────────────
/** Create a task with given assignments. Returns the display_code. */
function createTaskWithAssignees(opts: {
description: string;
groupId?: string;
creator: string;
assignees: string[];
dueDate?: string;
}): { id: number; displayCode: number } {
const creator = ensureUserExists(opts.creator, testDb)!;
const assigneeIds = opts.assignees
.map(uid => 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');
});
});
Loading…
Cancel
Save