import { handleCommand } from './command-handler'; import type { NormalizedMessage } from '@tower/types'; function makeMsg(overrides: Partial = {}): NormalizedMessage { return { platformMsgId: 'WA_MSG_001', sourceGroupJid: '120363043312345678@g.us', senderJid: '919876543210@s.whatsapp.net', senderName: 'Alice', content: '', accountId: 'acc-1', ...overrides, }; } const mockPrisma: any = { group: { findUnique: jest.fn() }, towerUser: { upsert: jest.fn() }, consentRecord: { findFirst: jest.fn(), create: jest.fn(), update: jest.fn(), updateMany: jest.fn() }, memberOptOut: { create: jest.fn() }, auditEvent: { create: jest.fn() }, }; const mockPool: any = { sendMessage: jest.fn().mockResolvedValue(undefined), }; describe('handleCommand', () => { beforeEach(() => { jest.clearAllMocks(); }); it('returns false for non-command messages', async () => { const handled = await handleCommand(makeMsg({ content: 'hello world' }), 'acc-1', mockPrisma, mockPool); expect(handled).toBe(false); expect(mockPool.sendMessage).not.toHaveBeenCalled(); }); describe('STOP', () => { it('creates opt-out and revokes consent for claimed group', async () => { mockPrisma.group.findUnique.mockResolvedValue({ id: 'grp-1', tenantId: 'tnt-1', claimStatus: 'CLAIMED' }); mockPrisma.towerUser.upsert.mockResolvedValue({ id: 'user-1' }); mockPrisma.consentRecord.updateMany.mockResolvedValue({ count: 1 }); mockPrisma.memberOptOut.create.mockResolvedValue({}); mockPrisma.auditEvent.create.mockResolvedValue({}); const handled = await handleCommand( makeMsg({ content: 'STOP' }), 'acc-1', mockPrisma, mockPool, ); expect(handled).toBe(true); expect(mockPrisma.consentRecord.updateMany).toHaveBeenCalled(); expect(mockPrisma.memberOptOut.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ reason: 'STOP_KEYWORD' }) }), ); expect(mockPool.sendMessage).toHaveBeenCalledWith( 'acc-1', '919876543210@s.whatsapp.net', expect.stringContaining("opted out"), ); }); it('is a no-op for non-claimed groups', async () => { mockPrisma.group.findUnique.mockResolvedValue({ id: 'grp-1', claimStatus: 'PENDING_CLAIM' }); const handled = await handleCommand( makeMsg({ content: 'STOP' }), 'acc-1', mockPrisma, mockPool, ); expect(handled).toBe(false); expect(mockPrisma.consentRecord.updateMany).not.toHaveBeenCalled(); }); }); describe('START', () => { it('re-grants default scopes', async () => { mockPrisma.group.findUnique.mockResolvedValue({ id: 'grp-1', tenantId: 'tnt-1', claimStatus: 'CLAIMED' }); mockPrisma.towerUser.upsert.mockResolvedValue({ id: 'user-1' }); mockPrisma.consentRecord.findFirst.mockResolvedValue({ id: 'c-1', scopes: ['INGEST'], retentionDays: 90, }); mockPrisma.consentRecord.update.mockResolvedValue({}); const handled = await handleCommand( makeMsg({ content: 'start' }), 'acc-1', mockPrisma, mockPool, ); expect(handled).toBe(true); expect(mockPrisma.consentRecord.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: 'c-1' }, data: expect.objectContaining({ status: 'GRANTED' }), }), ); }); }); describe('PORTAL', () => { it('sends an onboarding link', async () => { mockPrisma.group.findUnique.mockResolvedValue({ id: 'grp-1', tenantId: 'tnt-1', claimStatus: 'CLAIMED' }); mockPrisma.towerUser.upsert.mockResolvedValue({ id: 'user-1' }); const handled = await handleCommand( makeMsg({ content: 'portal' }), 'acc-1', mockPrisma, mockPool, ); expect(handled).toBe(true); expect(mockPool.sendMessage).toHaveBeenCalledWith( 'acc-1', '919876543210@s.whatsapp.net', expect.stringContaining('/onboard?token='), ); }); }); });