Files
tower/apps/worker/src/whatsapp/command-handler.test.ts
T
2026-06-09 02:02:40 +05:30

125 lines
4.1 KiB
TypeScript

import { handleCommand } from './command-handler';
import type { NormalizedMessage } from '@tower/types';
function makeMsg(overrides: Partial<NormalizedMessage> = {}): 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='),
);
});
});
});