125 lines
4.1 KiB
TypeScript
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='),
|
|
);
|
|
});
|
|
});
|
|
});
|