// tests/integration/twilioRoutes.test.ts // // End-to-end coverage of the Twilio-facing routes: signature validation, // TwiML response, and error-path resilience. The bot is stubbed — we only // care about the HTTP surface here. // // The Prisma-backed lookups fire after `res.send()`, so the response is // deterministic even without a populated database. The setup file points // DATABASE_URL at `file:./prisma/dev.db`, which exists and has the schema // applied from the initial migration. import { describe, expect, it, beforeAll, afterAll } from 'vitest'; import request from 'supertest'; import twilio from 'twilio'; import type { Express } from 'express'; import { createApp } from '../../src/app.ts'; import prisma from '../../src/services/PrismaService.ts'; const AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN!; const PUBLIC_BASE = process.env.WEBEX_PUBLIC_URL!.replace(/\/$/, ''); /** Minimal shape createApp/buildTwilioRouter actually reaches into. */ function makeStubBot() { return { webex: { messages: { create: async () => ({ id: 'stub' }), }, }, } as any; } function sign(path: string, params: Record): string { return twilio.getExpectedTwilioSignature(AUTH_TOKEN, `${PUBLIC_BASE}${path}`, params); } describe('Twilio routes (integration)', () => { let app: Express; beforeAll(() => { app = createApp(makeStubBot()); }); afterAll(async () => { try { await prisma.$disconnect(); } catch { /* ignore */ } }); describe('POST /sms', () => { it('rejects unsigned requests with 403', async () => { const res = await request(app) .post('/sms') .type('form') .send({ From: '+15551234567', To: '+15557654321', Body: 'hi', NumMedia: '0' }); expect(res.status).toBe(403); }); it('rejects requests with a bad signature', async () => { const res = await request(app) .post('/sms') .set('X-Twilio-Signature', 'nope') .type('form') .send({ From: '+15551234567', To: '+15557654321', Body: 'hi', NumMedia: '0' }); expect(res.status).toBe(403); }); it('accepts a signed inbound SMS and returns empty TwiML', async () => { const params = { From: '+15551234567', To: '+15557654321', Body: 'test body', NumMedia: '0' }; const res = await request(app) .post('/sms') .set('X-Twilio-Signature', sign('/sms', params)) .type('form') .send(params); expect(res.status).toBe(200); expect(res.headers['content-type']).toMatch(/xml/); expect(res.text).toContain(''); expect(res.text).toContain(''); }); }); describe('POST /callback', () => { it('rejects unsigned requests with 403', async () => { const res = await request(app) .post('/callback') .type('form') .send({ SmsSid: 'SM123', SmsStatus: 'delivered' }); expect(res.status).toBe(403); }); it('accepts a signed callback and returns TwiML', async () => { const params = { SmsSid: 'SM_test_' + Date.now(), SmsStatus: 'delivered' }; const res = await request(app) .post('/callback') .set('X-Twilio-Signature', sign('/callback', params)) .type('form') .send(params); expect(res.status).toBe(200); expect(res.text).toContain(''); }); it('accepts a signed failure callback with error code', async () => { const params = { SmsSid: 'SM_fail_' + Date.now(), SmsStatus: 'failed', ErrorCode: '30003', ErrorMessage: 'Unreachable', }; const res = await request(app) .post('/callback') .set('X-Twilio-Signature', sign('/callback', params)) .type('form') .send(params); expect(res.status).toBe(200); }); }); describe('GET /healthz', () => { it('returns 200 without a signature', async () => { const res = await request(app).get('/healthz'); expect(res.status).toBe(200); }); }); });