Files

119 lines
4.3 KiB
Python
Raw Permalink Normal View History

2026-08-18 15:41:28 -04:00
"""What actually goes on the wire to SendGrid and Twilio.
Asserted at the seam rather than mocked at the module boundary, because the
shape of these two requests is the part a provider silently rejects: a From
that is not the verified sender, a field Twilio spells `To` and not `to`.
"""
from __future__ import annotations
import base64
import json
import urllib.parse
from app import providers
def recorder(status: int, body: str = "{}", reply_headers: dict | None = None):
calls: list[dict] = []
def fake_post(url: str, *, data: bytes, headers: dict, timeout: float):
calls.append({"url": url, "data": data, "headers": headers, "timeout": timeout})
return providers.Response(status, body, reply_headers or {})
fake_post.calls = calls # type: ignore[attr-defined]
return fake_post
def test_email_is_addressed_from_the_verified_sender() -> None:
post = recorder(202, "", {"X-Message-Id": "abc"})
outcome = providers.send_email(
to="[email protected]",
subject="Due",
body="Bins",
api_key="sg-key",
sender="[email protected]",
timeout=5,
post=post,
)
sent = json.loads(post.calls[0]["data"])
assert post.calls[0]["url"] == providers.SENDGRID_URL
assert sent["from"]["email"] == "[email protected]"
assert sent["personalizations"][0]["to"] == [{"email": "[email protected]"}]
assert sent["content"][0]["type"] == "text/plain"
assert post.calls[0]["headers"]["Authorization"] == "Bearer sg-key"
assert outcome.ok and outcome.status == 202 and outcome.message_id == "abc"
def test_an_email_with_no_subject_still_sends() -> None:
post = recorder(202)
providers.send_email(
to="[email protected]", subject="", body="Bins", api_key="k", sender="[email protected]", timeout=5, post=post
)
assert json.loads(post.calls[0]["data"])["subject"] == "Notification"
def test_sendgrid_refusal_is_reported_not_raised() -> None:
post = recorder(403, json.dumps({"errors": [{"message": "The from address does not match a verified Sender"}]}))
outcome = providers.send_email(
to="[email protected]", subject="s", body="b", api_key="k", sender="[email protected]", timeout=5, post=post
)
assert not outcome.ok
assert outcome.status == 403
assert "verified Sender" in outcome.detail
def test_sms_is_form_encoded_with_twilio_field_names() -> None:
post = recorder(201, json.dumps({"sid": "SM9"}))
outcome = providers.send_sms(
to="+15551234567",
subject="Due",
body="Bins",
account_sid="AC123",
auth_token="secret",
sender="+15550001111",
timeout=5,
post=post,
)
fields = dict(urllib.parse.parse_qsl(post.calls[0]["data"].decode()))
assert post.calls[0]["url"].endswith("/Accounts/AC123/Messages.json")
assert fields == {"To": "+15551234567", "From": "+15550001111", "Body": "Due\n\nBins"}
expected = "Basic " + base64.b64encode(b"AC123:secret").decode()
assert post.calls[0]["headers"]["Authorization"] == expected
assert outcome.ok and outcome.message_id == "SM9"
def test_a_runaway_body_is_truncated_rather_than_billed_by_the_segment() -> None:
post = recorder(201, json.dumps({"sid": "SM9"}))
providers.send_sms(
to="+15551234567",
subject="",
body="x" * 50_000,
account_sid="AC1",
auth_token="t",
sender="+1555",
timeout=5,
post=post,
)
fields = dict(urllib.parse.parse_qsl(post.calls[0]["data"].decode()))
assert len(fields["Body"]) == providers.SMS_LIMIT
def test_twilio_refusal_carries_its_code() -> None:
post = recorder(400, json.dumps({"message": "To and From cannot be the same", "code": 21266}))
outcome = providers.send_sms(
to="+1555", subject="s", body="b", account_sid="AC1", auth_token="t", sender="+1555", timeout=5, post=post
)
assert not outcome.ok
assert outcome.status == 400
assert "21266" in outcome.detail
def test_an_unparseable_error_body_is_truncated_not_dumped() -> None:
post = recorder(500, "<html>" + "x" * 5000)
outcome = providers.send_email(
to="[email protected]", subject="s", body="b", api_key="k", sender="[email protected]", timeout=5, post=post
)
assert not outcome.ok
assert len(outcome.detail) <= 200