"""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": "person@example.test", "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"] == "person@example.test" assert sent[0]["sender"] == "no-reply@example.test" 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 b@example.test", "person@example", "person@.test", ""], ) 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", "person@example.test", "+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 "person@example.test" not in logged # nor the full address assert "p***@example.test" in logged @pytest.mark.parametrize( ("channel", "address", "expected"), [ ("email", "person@example.test", "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