/**
* 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(
'',
);
});
});