Files
channel-exit/tests/test_send.py
T
nirpa e4d6230dd1 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.
2026-08-18 15:41:28 -04:00

163 lines
6.2 KiB
Python

"""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