/** * Unit tests for `` TwiML generation. * * Every call connects over the media-stream transport; these tests exercise * the TwiML serializer only — handshake metadata encoding (path segments + * `` children), XML escaping, and structure. */ import { describe, expect, test } from "bun:test"; import { generateStreamTwiML } from "../calls/twilio-routes.js"; describe("generateStreamTwiML", () => { const callSessionId = "stream-session-1"; const streamUrl = "wss://test.example.com/webhooks/twilio/media-stream"; test("emits element with callSessionId as path segment", () => { const twiml = generateStreamTwiML(callSessionId, streamUrl); expect(twiml).toContain("", () => { const twiml = generateStreamTwiML(callSessionId, streamUrl); expect(twiml).toContain( ``, ); }); test("includes auth token as path segment and as when provided", () => { const twiml = generateStreamTwiML( callSessionId, streamUrl, "test-relay-token-123", ); // Token as path segment for gateway auth during WS upgrade expect(twiml).toContain( `url="wss://test.example.com/webhooks/twilio/media-stream/${callSessionId}/test-relay-token-123"`, ); // Token also in for Twilio start event payload expect(twiml).toContain( '', ); }); test("omits token from URL path and Parameter when not provided", () => { const twiml = generateStreamTwiML(callSessionId, streamUrl); expect(twiml).not.toContain('name="token"'); // URL should only have callSessionId as path segment, no token expect(twiml).toContain( `url="wss://test.example.com/webhooks/twilio/media-stream/${callSessionId}"`, ); }); test("includes custom parameters as elements", () => { const twiml = generateStreamTwiML(callSessionId, streamUrl, "tok", { verificationSessionId: "vs-123", }); expect(twiml).toContain( '', ); expect(twiml).toContain( ``, ); expect(twiml).toContain(''); }); test("callSessionId cannot be overridden by customParameters", () => { const twiml = generateStreamTwiML(callSessionId, streamUrl, undefined, { callSessionId: "attacker-session", }); // The real callSessionId must win over the custom parameter expect(twiml).toContain( ``, ); expect(twiml).not.toContain('value="attacker-session"'); // URL path must also have the correct callSessionId expect(twiml).toContain(`/media-stream/${callSessionId}`); expect(twiml).not.toContain("attacker-session"); }); test("does not include STT/TTS attributes on the Stream element", () => { const twiml = generateStreamTwiML(callSessionId, streamUrl); expect(twiml).not.toContain("transcriptionProvider="); expect(twiml).not.toContain("speechModel="); expect(twiml).not.toContain("interruptSensitivity="); expect(twiml).not.toContain("ttsProvider="); expect(twiml).not.toContain("voice="); expect(twiml).not.toContain("language="); }); test("wraps in valid TwiML structure", () => { const twiml = generateStreamTwiML(callSessionId, streamUrl); expect(twiml).toContain(''); expect(twiml).toContain(""); expect(twiml).toContain(""); expect(twiml).toContain(""); expect(twiml).toContain(""); expect(twiml).toContain(""); }); test("URL-encodes special characters in callSessionId path segment", () => { const specialId = "sess&id=1/2"; const twiml = generateStreamTwiML(specialId, streamUrl); // Special characters must be percent-encoded in the path segment expect(twiml).toContain("/media-stream/sess%26id%3D1%2F2"); // But the value should have the raw value (XML-escaped) expect(twiml).toContain( '', ); }); });