From 5c1d2f22517e1ee95e5015f6b79e038306e1b713 Mon Sep 17 00:00:00 2001 From: borja Date: Sat, 2 May 2026 00:20:29 +0200 Subject: [PATCH] fix: prevent user DMs from being treated as groups in gating handleGroupDiscovery and handleGroupEnforcement lacked isGroupId() guards, causing user JIDs (e.g. 1234567890@s.whatsapp.net) to be incorrectly registered as 'pending' groups in discover mode, and silently blocked in enforce mode. Added isGroupId() bail-out at the top of both functions, matching the existing pattern in ensureGroupActive(). Added 5 regression tests covering both modes for DMs and preserving correct group behavior. --- src/http/webhook-handler.ts | 2 + .../unit/server.dm-gating-regression.test.ts | 165 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 tests/unit/server.dm-gating-regression.test.ts diff --git a/src/http/webhook-handler.ts b/src/http/webhook-handler.ts index 2f99319..885f773 100644 --- a/src/http/webhook-handler.ts +++ b/src/http/webhook-handler.ts @@ -167,6 +167,7 @@ async function handleGroupDiscovery( senderId: string, isAdminCmd: boolean, ): Promise { + if (!isGroupId(groupId)) return false; // only applies to groups const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase(); if (mode !== 'discover') return false; @@ -188,6 +189,7 @@ async function handleGroupDiscovery( * Returns true if the message was blocked. */ function handleGroupEnforcement(groupId: string, isAdminCmd: boolean): boolean { + if (!isGroupId(groupId)) return false; // only applies to groups const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase(); if (mode !== 'enforce') return false; diff --git a/tests/unit/server.dm-gating-regression.test.ts b/tests/unit/server.dm-gating-regression.test.ts new file mode 100644 index 0000000..1a01782 --- /dev/null +++ b/tests/unit/server.dm-gating-regression.test.ts @@ -0,0 +1,165 @@ +/** + * Regression tests: DM (direct message) gating. + * + * Ensures that user DMs are NEVER treated as groups for gating purposes. + * A user JID (e.g. 1234567890@s.whatsapp.net) must not trigger group + * discovery (notify admins of a "new group") or group enforcement + * (silently block the message). + */ +import { describe, test, expect } from 'bun:test'; +import { WebhookServer } from '../../src/server'; +import { SimulatedResponseQueue } from '../helpers/queue'; +import { createTestRequest, registerServerTestLifecycle } from '../helpers/server-test-harness'; + +const testDb = registerServerTestLifecycle(); + +// ── Helpers ──────────────────────────────────────────────────────────── + +function makeDmPayload(text: string) { + return { + event: 'messages.upsert', + instance: 'test-instance', + data: { + key: { + remoteJid: '1234567890@s.whatsapp.net', // user JID, NOT a group + fromMe: false, + }, + message: { conversation: text }, + }, + }; +} + +function countAllowedGroups(): number { + try { + const row = testDb.prepare('SELECT COUNT(*) AS c FROM allowed_groups').get() as { c: number }; + return row.c; + } catch { + return 0; + } +} + +// ── Tests ────────────────────────────────────────────────────────────── + +describe('DM gating — user DMs must not be treated as groups', () => { + + test('discover mode: DM does NOT register user JID in allowed_groups', async () => { + process.env.GROUP_GATING_MODE = 'discover'; + + const request = createTestRequest(makeDmPayload('t ver mis')); + const response = await WebhookServer.handleRequest(request); + + expect(response.status).toBe(200); + + // No allowed_groups entry should exist for the user JID + const count = countAllowedGroups(); + expect(count).toBe(0); + + // Admin should NOT be notified about a "new group" + const out = SimulatedResponseQueue.get(); + const adminNotices = out.filter(r => r.message && (r.message as string).includes('Nuevo grupo detectado')); + expect(adminNotices.length).toBe(0); + }); + + test('enforce mode: DM is NOT blocked (user can still use commands)', async () => { + process.env.GROUP_GATING_MODE = 'enforce'; + + const request = createTestRequest(makeDmPayload('t ver mis')); + const response = await WebhookServer.handleRequest(request); + + expect(response.status).toBe(200); + + // Command should have processed and produced a response + const out = SimulatedResponseQueue.get(); + // "ver mis" in DM should produce at least one response + expect(out.length).toBeGreaterThan(0); + }); + + test('discover mode: unknown GROUP still gets discovered (regression check)', async () => { + process.env.GROUP_GATING_MODE = 'discover'; + process.env.NOTIFY_ADMINS_ON_DISCOVERY = 'false'; // don't spam admins in test + + // Ensure the group is NOT already in allowed_groups or active groups + testDb.exec('DELETE FROM allowed_groups WHERE group_id = \'unknown-group@g.us\''); + testDb.exec('DELETE FROM groups WHERE id = \'unknown-group@g.us\''); + + const payload = { + event: 'messages.upsert', + instance: 'test-instance', + data: { + key: { + remoteJid: 'unknown-group@g.us', // group JID + participant: '1234567890@s.whatsapp.net', + }, + message: { conversation: 'hola' }, + }, + }; + + const request = createTestRequest(payload); + await WebhookServer.handleRequest(request); + + // Should have registered the group as pending + const row = testDb.prepare('SELECT * FROM allowed_groups WHERE group_id = ?').get('unknown-group@g.us') as any; + expect(row).toBeDefined(); + expect(row.status).toBe('pending'); + }); + + test('enforce mode: non-allowed GROUP is still blocked (regression check)', async () => { + process.env.GROUP_GATING_MODE = 'enforce'; + + // Ensure the group is NOT allowed + testDb.exec('DELETE FROM allowed_groups WHERE group_id = \'blocked-group@g.us\''); + testDb.exec('DELETE FROM groups WHERE id = \'blocked-group@g.us\''); + // Add it as active so it passes ensureGroupActive + testDb.exec(` + INSERT OR IGNORE INTO groups (id, community_id, name, active) + VALUES ('blocked-group@g.us', 'test-community', 'Blocked Group', 1) + `); + + const payload = { + event: 'messages.upsert', + instance: 'test-instance', + data: { + key: { + remoteJid: 'blocked-group@g.us', + participant: '1234567890@s.whatsapp.net', + }, + message: { conversation: 't ver mis' }, + }, + }; + + const request = createTestRequest(payload); + await WebhookServer.handleRequest(request); + + // Message should be silently blocked (no response) + const out = SimulatedResponseQueue.get(); + expect(out.length).toBe(0); + }); + + test('enforce mode: allowed GROUP can still use commands (regression check)', async () => { + process.env.GROUP_GATING_MODE = 'enforce'; + + testDb.exec(` + INSERT OR REPLACE INTO allowed_groups (group_id, label, status) + VALUES ('group-id@g.us', 'Test Group', 'allowed') + `); + + const payload = { + event: 'messages.upsert', + instance: 'test-instance', + data: { + key: { + remoteJid: 'group-id@g.us', + participant: '1234567890@s.whatsapp.net', + }, + message: { conversation: 't n Test tarea mañana' }, + }, + }; + + const request = createTestRequest(payload); + await WebhookServer.handleRequest(request); + + const out = SimulatedResponseQueue.get(); + expect(out.length).toBeGreaterThan(0); + }); + +});