The estate's one channel exit

Services that need to reach a person hand the message here instead of each
carrying a SendGrid key. Email goes to SendGrid, SMS to Twilio; whatsapp and
push answer 501 rather than pretending.

It binds loopback and the docker bridge only. An authenticated relay on the
public internet is an open spam relay the moment the token leaks, and that
token is copied into every consumer's environment.

The log names a channel, a redacted address and the provider's answer. Never
the subject, never the body.
This commit is contained in:
nirpa
2026-08-18 15:41:28 -04:00
commit e4d6230dd1
17 changed files with 913 additions and 0 deletions
View File
+50
View File
@@ -0,0 +1,50 @@
"""Shared fixtures, and one guarantee.
The autouse fixture below is the important part: a suite for a service whose
entire job is sending messages to real people must not be able to send one.
Anything that reaches for a socket fails loudly instead of quietly costing
money or waking someone up.
"""
from __future__ import annotations
import urllib.request
import pytest
from fastapi.testclient import TestClient
from app.config import Settings
from app.main import create_app
TOKEN = "test-token"
@pytest.fixture(autouse=True)
def no_network(monkeypatch: pytest.MonkeyPatch) -> None:
def refuse(*args, **kwargs):
raise AssertionError("a test tried to reach the network")
monkeypatch.setattr(urllib.request, "urlopen", refuse)
@pytest.fixture
def settings() -> Settings:
return Settings(
token=TOKEN,
sendgrid_key="sg-key",
email_from="[email protected]",
twilio_sid="AC123",
twilio_token="twilio-secret",
twilio_from="+15550001111",
timeout=1.0,
)
@pytest.fixture
def client(settings: Settings) -> TestClient:
return TestClient(create_app(settings))
@pytest.fixture
def auth() -> dict[str, str]:
return {"Authorization": f"Bearer {TOKEN}"}
+118
View File
@@ -0,0 +1,118 @@
"""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
+162
View File
@@ -0,0 +1,162 @@
"""The gate: who may send, on what channel, to what address.
Every case here is a way this service could become an open relay, a bill, or a
message to the wrong person.
"""
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from app import providers
from app.config import Settings
from app.main import create_app, redact
from tests.conftest import TOKEN
@pytest.fixture
def sent(monkeypatch: pytest.MonkeyPatch) -> list[dict]:
"""Record what would have gone to a provider, and send nothing."""
calls: list[dict] = []
def fake_email(**kwargs):
calls.append({"provider": "sendgrid", **kwargs})
return providers.Outcome(ok=True, status=202, detail="accepted", message_id="msg-1")
def fake_sms(**kwargs):
calls.append({"provider": "twilio", **kwargs})
return providers.Outcome(ok=True, status=201, detail="accepted", message_id="SM1")
monkeypatch.setattr(providers, "send_email", fake_email)
monkeypatch.setattr(providers, "send_sms", fake_sms)
return calls
def body(**over) -> dict:
payload = {"channel": "email", "to": "[email protected]", "subject": "Due", "body": "Take the bins out"}
payload.update(over)
return payload
def test_healthz_needs_no_token_and_reports_wiring(client: TestClient) -> None:
reply = client.get("/healthz")
assert reply.status_code == 200
assert reply.json()["channels"] == {"email": True, "sms": True}
@pytest.mark.parametrize(
"headers",
[{}, {"Authorization": "Bearer "}, {"Authorization": f"Bearer {TOKEN}x"}, {"Authorization": TOKEN}],
ids=["absent", "empty", "wrong", "unprefixed"],
)
def test_a_send_without_the_right_bearer_is_refused(client: TestClient, sent: list, headers: dict) -> None:
reply = client.post("/send", json=body(), headers=headers)
assert reply.status_code == 401
assert sent == [] # and nothing was handed to a provider
def test_service_refuses_to_start_without_a_token() -> None:
with pytest.raises(RuntimeError):
create_app(Settings(token=""))
def test_email_goes_to_sendgrid_from_the_operator_sender(client: TestClient, auth: dict, sent: list) -> None:
reply = client.post("/send", json=body(), headers=auth)
assert reply.status_code == 200
assert reply.json() == {"ok": True, "channel": "email", "provider_status": 202, "id": "msg-1"}
assert sent[0]["provider"] == "sendgrid"
assert sent[0]["to"] == "[email protected]"
assert sent[0]["sender"] == "[email protected]"
def test_sms_goes_to_twilio(client: TestClient, auth: dict, sent: list) -> None:
reply = client.post("/send", json=body(channel="sms", to="+15551234567"), headers=auth)
assert reply.status_code == 200
assert sent[0]["provider"] == "twilio"
assert sent[0]["sender"] == "+15550001111"
@pytest.mark.parametrize("channel", ["whatsapp", "push"])
def test_unwired_channels_say_so_rather_than_pretending(client: TestClient, auth: dict, sent: list, channel: str) -> None:
reply = client.post("/send", json=body(channel=channel, to="+15551234567"), headers=auth)
assert reply.status_code == 501
assert channel in reply.json()["error"]
assert sent == []
def test_an_unknown_channel_is_rejected(client: TestClient, auth: dict, sent: list) -> None:
reply = client.post("/send", json=body(channel="carrier-pigeon"), headers=auth)
assert reply.status_code == 400
assert sent == []
@pytest.mark.parametrize(
"address",
["+15551234567", "person", "person@", "@example.test", "a [email protected]", "person@example", "[email protected]", ""],
)
def test_email_channel_rejects_anything_that_is_not_an_email(client: TestClient, auth: dict, sent: list, address: str) -> None:
reply = client.post("/send", json=body(to=address), headers=auth)
assert reply.status_code == 400
assert sent == []
@pytest.mark.parametrize(
"number",
["15551234567", "+0155512345", "+1555", "[email protected]", "+1555123456789012", "+1 555 123 4567", ""],
)
def test_sms_channel_rejects_anything_that_is_not_e164(client: TestClient, auth: dict, sent: list, number: str) -> None:
reply = client.post("/send", json=body(channel="sms", to=number), headers=auth)
assert reply.status_code == 400
assert sent == []
def test_an_empty_message_is_not_worth_sending(client: TestClient, auth: dict, sent: list) -> None:
reply = client.post("/send", json=body(subject="", body=""), headers=auth)
assert reply.status_code == 400
assert sent == []
def test_a_body_that_is_not_json_is_rejected(client: TestClient, auth: dict) -> None:
reply = client.post("/send", content=b"not json", headers={**auth, "Content-Type": "application/json"})
assert reply.status_code == 400
def test_a_channel_without_credentials_answers_503(auth: dict, sent: list) -> None:
client = TestClient(create_app(Settings(token=TOKEN, sendgrid_key="", email_from="")))
reply = client.post("/send", json=body(), headers=auth)
assert reply.status_code == 503
assert sent == []
def test_a_provider_refusal_surfaces_as_502_with_its_status(client: TestClient, auth: dict, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
providers,
"send_email",
lambda **_: providers.Outcome(ok=False, status=403, detail="sender not verified"),
)
reply = client.post("/send", json=body(), headers=auth)
assert reply.status_code == 502
assert reply.json()["provider_status"] == 403
def test_the_message_never_reaches_the_log(client: TestClient, auth: dict, sent: list, caplog) -> None:
with caplog.at_level("INFO"):
client.post("/send", json=body(subject="Divorce hearing", body="10am, courthouse"), headers=auth)
logged = caplog.text
assert "Divorce hearing" not in logged
assert "courthouse" not in logged
assert "[email protected]" not in logged # nor the full address
assert "p***@example.test" in logged
@pytest.mark.parametrize(
("channel", "address", "expected"),
[
("email", "[email protected]", "p***@example.test"),
("sms", "+15551234567", "***4567"),
("sms", "+123", "***"),
],
)
def test_redaction_keeps_enough_to_recognise_a_delivery(channel: str, address: str, expected: str) -> None:
assert redact(channel, address) == expected