dograh/api/tests/telephony/twilio/test_routes.py
Abhishek 3892b58486
feat: add ultravox realtime and fix signature issue in telephony (#345)
* feat: add ultravox realtime and fix signature issue in telephony

- Add UltraVox realtime
- Fix signature issue on telephony

* fix: fix regression for wss_backend_endpoint
2026-05-23 12:51:55 +05:30

253 lines
7.4 KiB
Python

from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from urllib.parse import urlencode
import pytest
from fastapi import HTTPException
from starlette.requests import Request
from twilio.request_validator import RequestValidator
from api.services.telephony.providers.twilio.provider import TwilioProvider
from api.services.telephony.providers.twilio.routes import (
handle_twilio_status_callback,
handle_twiml_webhook,
)
def _provider() -> TwilioProvider:
return TwilioProvider(
{
"account_sid": "AC123",
"auth_token": "twilio-auth-token",
"from_numbers": ["+15551230002"],
}
)
def _request(
*,
path: str,
query: dict[str, str | int],
form_data: dict[str, str],
headers: dict[str, str] | None = None,
) -> Request:
body = urlencode(form_data).encode("utf-8")
query_string = urlencode(query).encode("utf-8")
request_headers = [
(b"content-type", b"application/x-www-form-urlencoded"),
*[
(name.lower().encode("ascii"), value.encode("ascii"))
for name, value in (headers or {}).items()
],
]
async def receive():
return {
"type": "http.request",
"body": body,
"more_body": False,
}
return Request(
{
"type": "http",
"method": "POST",
"scheme": "https",
"server": ("example.test", 443),
"path": path,
"query_string": query_string,
"headers": request_headers,
},
receive,
)
def _signature(
provider: TwilioProvider,
*,
path: str,
query: dict[str, str | int],
form_data: dict[str, str],
) -> str:
url = f"https://example.test{path}"
if query:
url = f"{url}?{urlencode(query)}"
validator = RequestValidator(provider.auth_token)
return validator.compute_signature(url, form_data)
@pytest.mark.asyncio
async def test_twiml_route_accepts_valid_signature_with_extra_query_param():
provider = _provider()
query = {
"workflow_id": 7,
"user_id": 8,
"workflow_run_id": 123,
"campaign_id": 42,
"organization_id": 11,
}
form_data = {"CallSid": "CA123", "CallStatus": "in-progress"}
request = _request(
path="/api/v1/telephony/twiml",
query=query,
form_data=form_data,
headers={
"x-twilio-signature": _signature(
provider,
path="/api/v1/telephony/twiml",
query=query,
form_data=form_data,
)
},
)
with (
patch("api.services.telephony.providers.twilio.routes.db_client") as db_client,
patch(
"api.services.telephony.providers.twilio.routes.get_telephony_provider_for_run",
new_callable=AsyncMock,
return_value=provider,
),
patch.object(
provider,
"get_webhook_response",
new_callable=AsyncMock,
return_value="<Response/>",
) as get_webhook_response,
):
db_client.get_workflow_run_by_id = AsyncMock(
return_value=SimpleNamespace(id=123)
)
response = await handle_twiml_webhook(
workflow_id=7,
user_id=8,
workflow_run_id=123,
organization_id=11,
request=request,
)
assert response.body == b"<Response/>"
get_webhook_response.assert_awaited_once_with(7, 8, 123)
@pytest.mark.asyncio
async def test_twiml_route_rejects_missing_signature():
provider = _provider()
request = _request(
path="/api/v1/telephony/twiml",
query={
"workflow_id": 7,
"user_id": 8,
"workflow_run_id": 123,
"organization_id": 11,
},
form_data={"CallSid": "CA123"},
)
with (
patch("api.services.telephony.providers.twilio.routes.db_client") as db_client,
patch(
"api.services.telephony.providers.twilio.routes.get_telephony_provider_for_run",
new_callable=AsyncMock,
return_value=provider,
),
):
db_client.get_workflow_run_by_id = AsyncMock(
return_value=SimpleNamespace(id=123)
)
with pytest.raises(HTTPException) as exc_info:
await handle_twiml_webhook(
workflow_id=7,
user_id=8,
workflow_run_id=123,
organization_id=11,
request=request,
)
assert exc_info.value.status_code == 401
assert exc_info.value.detail == "Invalid webhook signature"
@pytest.mark.asyncio
async def test_twilio_status_callback_rejects_legacy_header_name():
provider = _provider()
form_data = {"CallSid": "CA123", "CallStatus": "completed"}
request = _request(
path="/api/v1/telephony/twilio/status-callback/123",
query={},
form_data=form_data,
headers={"x-webhook-signature": "not-a-twilio-signature"},
)
with (
patch("api.services.telephony.providers.twilio.routes.db_client") as db_client,
patch(
"api.services.telephony.providers.twilio.routes.get_telephony_provider_for_run",
new_callable=AsyncMock,
return_value=provider,
),
patch(
"api.services.telephony.providers.twilio.routes._process_status_update",
new_callable=AsyncMock,
) as process_status,
):
db_client.get_workflow_run_by_id = AsyncMock(
return_value=SimpleNamespace(workflow_id=7)
)
db_client.get_workflow_by_id = AsyncMock(
return_value=SimpleNamespace(organization_id=11)
)
with pytest.raises(HTTPException) as exc_info:
await handle_twilio_status_callback(workflow_run_id=123, request=request)
assert exc_info.value.status_code == 401
assert exc_info.value.detail == "Invalid webhook signature"
process_status.assert_not_awaited()
@pytest.mark.asyncio
async def test_twilio_status_callback_accepts_valid_signature():
provider = _provider()
form_data = {"CallSid": "CA123", "CallStatus": "completed"}
request = _request(
path="/api/v1/telephony/twilio/status-callback/123",
query={},
form_data=form_data,
headers={
"x-twilio-signature": _signature(
provider,
path="/api/v1/telephony/twilio/status-callback/123",
query={},
form_data=form_data,
)
},
)
with (
patch("api.services.telephony.providers.twilio.routes.db_client") as db_client,
patch(
"api.services.telephony.providers.twilio.routes.get_telephony_provider_for_run",
new_callable=AsyncMock,
return_value=provider,
),
patch(
"api.services.telephony.providers.twilio.routes._process_status_update",
new_callable=AsyncMock,
) as process_status,
):
db_client.get_workflow_run_by_id = AsyncMock(
return_value=SimpleNamespace(workflow_id=7)
)
db_client.get_workflow_by_id = AsyncMock(
return_value=SimpleNamespace(organization_id=11)
)
result = await handle_twilio_status_callback(
workflow_run_id=123, request=request
)
assert result == {"status": "success"}
process_status.assert_awaited_once()