From 00a587fcce83b8e092d409b82b3775fd1de6e4b5 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 31 Jul 2026 18:04:47 +0800 Subject: [PATCH] test(voice-call): cover terminal webhook provider cleanup --- extensions/voice-call/src/webhook.test.ts | 188 ++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/extensions/voice-call/src/webhook.test.ts b/extensions/voice-call/src/webhook.test.ts index 08f036b29304..ef31674e352a 100644 --- a/extensions/voice-call/src/webhook.test.ts +++ b/extensions/voice-call/src/webhook.test.ts @@ -198,13 +198,53 @@ function expectWebhookUrl(url: string, expectedPath: string) { function expectNoTwilioStreamState(providerLocal: TwilioProvider) { const state = providerLocal as unknown as { + callStreamMap: Map; streamAuthTokens: Map; activeStreamCalls: Set; }; + expect(state.callStreamMap.size).toBe(0); expect(state.streamAuthTokens.size).toBe(0); expect(state.activeStreamCalls.size).toBe(0); } +function expectTwilioCallStateReleased( + providerLocal: TwilioProvider, + params: { callId: string; providerCallId: string }, +) { + const state = providerLocal as unknown as { + callWebhookUrls: Map; + callStreamMap: Map; + streamAuthTokens: Map; + twimlStorage: Map; + notifyCalls: Set; + activeStreamCalls: Set; + }; + expect(state.callWebhookUrls.has(params.providerCallId)).toBe(false); + expect(state.callStreamMap.has(params.providerCallId)).toBe(false); + expect(state.streamAuthTokens.has(params.providerCallId)).toBe(false); + expect(state.twimlStorage.has(params.callId)).toBe(false); + expect(state.notifyCalls.has(params.callId)).toBe(false); + expect(state.activeStreamCalls.has(params.providerCallId)).toBe(false); +} + +function expectPlivoCallStateReleased( + providerLocal: PlivoProvider, + params: { callId: string; requestUuid: string; callUuid: string }, +) { + const state = providerLocal as unknown as { + requestUuidToCallUuid: Map; + callIdToWebhookUrl: Map; + callUuidToWebhookUrl: Map; + pendingSpeakByCallId: Map; + pendingListenByCallId: Map; + }; + expect(state.requestUuidToCallUuid.has(params.requestUuid)).toBe(false); + expect(state.callIdToWebhookUrl.has(params.callId)).toBe(false); + expect(state.callUuidToWebhookUrl.has(params.callUuid)).toBe(false); + expect(state.pendingSpeakByCallId.has(params.callId)).toBe(false); + expect(state.pendingListenByCallId.has(params.callId)).toBe(false); +} + async function expectTwilioReplayTwiML(response: Response) { expect(response.status).toBe(200); expect(response.headers.get("content-type")).toContain("text/xml"); @@ -924,6 +964,154 @@ describe("VoiceCallWebhookServer path matching", () => { }); describe("VoiceCallWebhookServer replay handling", () => { + it("releases Twilio provider state through a terminal webhook before replay ack", async () => { + const callId = "call-webhook-terminal-twilio"; + const providerCallId = "CA-webhook-terminal-twilio"; + const twilioProvider = new TwilioProvider( + { accountSid: "AC123", authToken: "secret" }, + { + publicUrl: "https://example.test/voice/webhook", + streamPath: "/voice/stream", + skipVerification: true, + }, + ); + const state = twilioProvider as unknown as { + callWebhookUrls: Map; + callStreamMap: Map; + streamAuthTokens: Map; + twimlStorage: Map; + notifyCalls: Set; + activeStreamCalls: Set; + }; + state.callWebhookUrls.set( + providerCallId, + `https://example.test/voice/webhook?callId=${callId}`, + ); + state.callStreamMap.set(providerCallId, "MZ-webhook-terminal"); + state.streamAuthTokens.set(providerCallId, "stream-token"); + state.twimlStorage.set(callId, "Hello"); + state.notifyCalls.add(callId); + state.activeStreamCalls.add(providerCallId); + + const parseWebhookEvent = vi.spyOn(twilioProvider, "parseWebhookEvent"); + const { manager, processEvent } = createManager([]); + const config = createConfig({ + provider: "twilio", + skipSignatureVerification: true, + twilio: { accountSid: "AC123", authToken: "secret" }, + }); + const server = new VoiceCallWebhookServer(config, manager, twilioProvider); + + try { + const baseUrl = await server.start(); + const requestUrl = requireBoundRequestUrl(server, baseUrl); + requestUrl.searchParams.set("callId", callId); + requestUrl.searchParams.set("type", "status"); + const body = `CallSid=${providerCallId}&CallStatus=completed&Direction=outbound-api`; + + const first = await fetch(requestUrl.toString(), { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body, + }); + expect(first.status).toBe(200); + expect(await first.text()).toBe( + '', + ); + expectTwilioCallStateReleased(twilioProvider, { callId, providerCallId }); + expect(parseWebhookEvent).toHaveBeenCalledTimes(1); + expect(processEvent).toHaveBeenCalledTimes(1); + + const replay = await fetch(requestUrl.toString(), { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body, + }); + await expectTwilioReplayTwiML(replay); + expectTwilioCallStateReleased(twilioProvider, { callId, providerCallId }); + expect(parseWebhookEvent).toHaveBeenCalledTimes(1); + expect(processEvent).toHaveBeenCalledTimes(1); + } finally { + parseWebhookEvent.mockRestore(); + await server.stop(); + } + }); + + it("releases Plivo provider state through a terminal webhook before replay ack", async () => { + const callId = "call-webhook-terminal-plivo"; + const requestUuid = "request-webhook-terminal-plivo"; + const callUuid = "call-uuid-webhook-terminal-plivo"; + const plivoProvider = new PlivoProvider( + { + authId: "MA000000000000000000", + authToken: "test-token", + }, + { skipVerification: true }, + ); + const state = plivoProvider as unknown as { + requestUuidToCallUuid: Map; + callIdToWebhookUrl: Map; + callUuidToWebhookUrl: Map; + pendingSpeakByCallId: Map; + pendingListenByCallId: Map; + }; + state.requestUuidToCallUuid.set(requestUuid, callUuid); + state.callIdToWebhookUrl.set(callId, "https://example.test/voice/webhook"); + state.callUuidToWebhookUrl.set(callUuid, "https://example.test/voice/webhook"); + state.pendingSpeakByCallId.set(callId, { text: "Hello" }); + state.pendingListenByCallId.set(callId, { language: "en-US" }); + + const parseWebhookEvent = vi.spyOn(plivoProvider, "parseWebhookEvent"); + const { manager, processEvent } = createManager([]); + const config = createConfig({ + provider: "plivo", + skipSignatureVerification: true, + plivo: { + authId: "MA000000000000000000", + authToken: "test-token", + }, + }); + const server = new VoiceCallWebhookServer(config, manager, plivoProvider); + + try { + const baseUrl = await server.start(); + const requestUrl = requireBoundRequestUrl(server, baseUrl); + requestUrl.searchParams.set("provider", "plivo"); + requestUrl.searchParams.set("flow", "hangup"); + requestUrl.searchParams.set("callId", callId); + const body = `CallUUID=${callUuid}&RequestUUID=${requestUuid}&CallStatus=completed&Direction=outbound`; + + const first = await fetch(requestUrl.toString(), { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body, + }); + expect(first.status).toBe(200); + expect(await first.text()).toBe( + '', + ); + expectPlivoCallStateReleased(plivoProvider, { callId, requestUuid, callUuid }); + expect(parseWebhookEvent).toHaveBeenCalledTimes(1); + expect(processEvent).toHaveBeenCalledTimes(1); + + const replay = await fetch(requestUrl.toString(), { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body, + }); + expect(replay.status).toBe(200); + expect(await replay.text()).toBe( + '', + ); + expectPlivoCallStateReleased(plivoProvider, { callId, requestUuid, callUuid }); + expect(parseWebhookEvent).toHaveBeenCalledTimes(1); + expect(processEvent).toHaveBeenCalledTimes(1); + } finally { + parseWebhookEvent.mockRestore(); + await server.stop(); + } + }); + it("acknowledges replayed webhook requests and skips event side effects", async () => { const parseWebhookEvent = vi.fn(() => ({ events: [