Files
channel-exit/tests/test_send.py
T

163 lines
6.2 KiB
Python
Raw Normal View History

2026-08-18 15:41:28 -04:00
"""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