4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
268 lines
7.6 KiB
Python
268 lines
7.6 KiB
Python
"""Tests for integrations.twilio.delivery — Twilio SMS transport."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from integrations.twilio.delivery import post_twilio_sms, send_twilio_sms_report
|
|
from platform.notifications.delivery_transport import DeliveryResponse
|
|
|
|
|
|
def _success_response(**kwargs: Any) -> DeliveryResponse:
|
|
defaults: dict[str, Any] = {"ok": True, "status_code": 201, "data": {"sid": "SM1"}}
|
|
defaults.update(kwargs)
|
|
return DeliveryResponse(**defaults)
|
|
|
|
|
|
def _error_response(**kwargs: Any) -> DeliveryResponse:
|
|
defaults: dict[str, Any] = {
|
|
"ok": True,
|
|
"status_code": 400,
|
|
"data": {"message": "Invalid 'To' parameter"},
|
|
"text": "Bad Request",
|
|
}
|
|
defaults.update(kwargs)
|
|
return DeliveryResponse(**defaults)
|
|
|
|
|
|
def test_post_twilio_sms_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _fake_post_form(url: str, data: dict[str, str], **kwargs: Any) -> DeliveryResponse:
|
|
captured["url"] = url
|
|
captured["data"] = data
|
|
captured.update(kwargs)
|
|
return _success_response()
|
|
|
|
monkeypatch.setattr("integrations.twilio.delivery.post_form", _fake_post_form)
|
|
|
|
success, error, sid = post_twilio_sms(
|
|
to="+14155550000",
|
|
text="ping",
|
|
account_sid="AC1",
|
|
auth_token="tok",
|
|
from_number="+14155551111",
|
|
)
|
|
|
|
assert (success, error, sid) == (True, "", "SM1")
|
|
assert captured["url"].endswith("/Accounts/AC1/Messages.json")
|
|
assert captured["data"]["To"] == "+14155550000"
|
|
assert captured["data"]["From"] == "+14155551111"
|
|
assert captured["data"]["Body"] == "ping"
|
|
assert "MessagingServiceSid" not in captured["data"]
|
|
assert "StatusCallback" not in captured["data"]
|
|
assert captured["auth"] == ("AC1", "tok")
|
|
|
|
|
|
def test_post_twilio_sms_with_messaging_service_overrides_from(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _fake_post_form(url: str, data: dict[str, str], **kwargs: Any) -> DeliveryResponse:
|
|
captured["data"] = data
|
|
return _success_response(data={"sid": "SM2"})
|
|
|
|
monkeypatch.setattr("integrations.twilio.delivery.post_form", _fake_post_form)
|
|
|
|
post_twilio_sms(
|
|
to="+14155550000",
|
|
text="hi",
|
|
account_sid="AC1",
|
|
auth_token="tok",
|
|
from_number="+14155551111",
|
|
messaging_service_sid="MG123",
|
|
)
|
|
|
|
assert captured["data"]["MessagingServiceSid"] == "MG123"
|
|
assert "From" not in captured["data"]
|
|
|
|
|
|
def test_post_twilio_sms_status_callback_passes_through(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _fake_post_form(url: str, data: dict[str, str], **kwargs: Any) -> DeliveryResponse:
|
|
captured["data"] = data
|
|
return _success_response(data={"sid": "SM3"})
|
|
|
|
monkeypatch.setattr("integrations.twilio.delivery.post_form", _fake_post_form)
|
|
|
|
post_twilio_sms(
|
|
to="+14155550000",
|
|
text="hi",
|
|
account_sid="AC1",
|
|
auth_token="tok",
|
|
from_number="+14155551111",
|
|
status_callback="https://example.com/webhooks/twilio/status",
|
|
)
|
|
|
|
assert captured["data"]["StatusCallback"] == "https://example.com/webhooks/twilio/status"
|
|
|
|
|
|
def test_post_twilio_sms_missing_sender_fails() -> None:
|
|
success, error, sid = post_twilio_sms(
|
|
to="+14155550000",
|
|
text="hi",
|
|
account_sid="AC1",
|
|
auth_token="tok",
|
|
)
|
|
|
|
assert success is False
|
|
assert "from_number" in error
|
|
assert sid == ""
|
|
|
|
|
|
def test_post_twilio_sms_transport_failure_redacts_token(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(
|
|
"integrations.twilio.delivery.post_form",
|
|
lambda *_a, **_kw: DeliveryResponse(
|
|
ok=False, error="auth header tok-leak failed", exc_type="RuntimeError"
|
|
),
|
|
)
|
|
|
|
success, error, sid = post_twilio_sms(
|
|
to="+14155550000",
|
|
text="hi",
|
|
account_sid="AC1",
|
|
auth_token="tok-leak",
|
|
from_number="+14155551111",
|
|
)
|
|
|
|
assert success is False
|
|
assert "tok-leak" not in error
|
|
assert "<redacted>" in error
|
|
assert sid == ""
|
|
|
|
|
|
def test_post_twilio_sms_api_error_returns_message(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(
|
|
"integrations.twilio.delivery.post_form",
|
|
lambda *_a, **_kw: _error_response(),
|
|
)
|
|
|
|
success, error, sid = post_twilio_sms(
|
|
to="bad",
|
|
text="hi",
|
|
account_sid="AC1",
|
|
auth_token="tok",
|
|
from_number="+14155551111",
|
|
)
|
|
|
|
assert success is False
|
|
assert "Invalid 'To' parameter" in error
|
|
assert sid == ""
|
|
|
|
|
|
def test_post_twilio_sms_api_error_message_key_fallback(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Twilio sometimes uses error_message instead of message."""
|
|
monkeypatch.setattr(
|
|
"integrations.twilio.delivery.post_form",
|
|
lambda *_a, **_kw: DeliveryResponse(
|
|
ok=True,
|
|
status_code=400,
|
|
data={"error_message": "Queue overflow"},
|
|
text="",
|
|
),
|
|
)
|
|
|
|
success, error, sid = post_twilio_sms(
|
|
to="+14155550000",
|
|
text="hi",
|
|
account_sid="AC1",
|
|
auth_token="tok",
|
|
from_number="+14155551111",
|
|
)
|
|
|
|
assert success is False
|
|
assert "Queue overflow" in error
|
|
assert sid == ""
|
|
|
|
|
|
def test_send_twilio_sms_report_truncates(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
captured: dict[str, Any] = {}
|
|
|
|
def _fake_post_form(url: str, data: dict[str, str], **kwargs: Any) -> DeliveryResponse:
|
|
captured["data"] = data
|
|
return _success_response(data={"sid": "SM6"})
|
|
|
|
monkeypatch.setattr("integrations.twilio.delivery.post_form", _fake_post_form)
|
|
|
|
send_twilio_sms_report(
|
|
report="X" * 5000,
|
|
sms_ctx={
|
|
"account_sid": "AC1",
|
|
"auth_token": "tok",
|
|
"from_number": "+14155551111",
|
|
"to": "+14155550000",
|
|
},
|
|
)
|
|
|
|
assert len(captured["data"]["Body"]) <= 1600
|
|
assert captured["data"]["Body"].endswith("…")
|
|
|
|
|
|
def test_send_twilio_sms_report_missing_creds() -> None:
|
|
success, error, sid = send_twilio_sms_report(
|
|
report="hi",
|
|
sms_ctx={"account_sid": "AC1"},
|
|
)
|
|
|
|
assert success is False
|
|
assert "Missing" in error
|
|
assert sid == ""
|
|
|
|
|
|
def test_send_twilio_sms_report_missing_sender() -> None:
|
|
success, error, sid = send_twilio_sms_report(
|
|
report="hi",
|
|
sms_ctx={
|
|
"account_sid": "AC1",
|
|
"auth_token": "tok",
|
|
"to": "+14155550000",
|
|
},
|
|
)
|
|
|
|
assert success is False
|
|
assert "from_number" in error
|
|
assert sid == ""
|
|
|
|
|
|
def test_send_twilio_sms_report_success_returns_sid(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(
|
|
"integrations.twilio.delivery.post_form",
|
|
lambda *_a, **_kw: _success_response(data={"sid": "SM-OK"}),
|
|
)
|
|
|
|
success, error, sid = send_twilio_sms_report(
|
|
report="investigation summary",
|
|
sms_ctx={
|
|
"account_sid": "AC1",
|
|
"auth_token": "tok",
|
|
"messaging_service_sid": "MG1",
|
|
"to": "+14155550000",
|
|
},
|
|
)
|
|
|
|
assert success is True
|
|
assert error == ""
|
|
assert sid == "SM-OK"
|
|
|
|
|
|
class TestTwilioDelegatesToSharedTransport:
|
|
"""Twilio delivery must use post_form from delivery_transport,
|
|
not import httpx directly."""
|
|
|
|
def test_module_does_not_import_httpx(self) -> None:
|
|
from integrations.twilio import delivery as twilio_delivery
|
|
|
|
assert not hasattr(twilio_delivery, "httpx"), (
|
|
"twilio_delivery should not import httpx directly — "
|
|
"it must go through delivery_transport.post_form"
|
|
)
|