CI / test (push) Successful in 5s
The estate's SendGrid key is send-only: mail.send and the batch scopes, with no suppression.read, no bounces.read, no stats. So email delivery cannot be reconciled the way SMS now is. The tempting move is to check SMS and quietly say nothing about email. That is precisely how a gap disappears - the daily report looks clean and the unchecked channel stops being a question anyone asks. It now reports 'email delivery NOT VERIFIED' with the exact scope that would fix it, every day, until somebody does. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
139 lines
5.2 KiB
Python
139 lines
5.2 KiB
Python
"""Reconciliation.
|
|
|
|
These exist because the estate spent seven months treating "the provider
|
|
accepted it" as "the person got it". Every test here is about telling those two
|
|
apart.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime, timezone
|
|
|
|
import pytest
|
|
|
|
from app.providers import Response
|
|
from app.reconcile import Message, describe, fetch, reconcile, window_start
|
|
|
|
|
|
def ledger(*rows) -> Response:
|
|
return Response(status=200, body=json.dumps({"messages": list(rows)}), headers={})
|
|
|
|
|
|
def row(sid="SM1", to="+15550001111", status="delivered", error=None):
|
|
return {"sid": sid, "to": to, "status": status, "error_code": error,
|
|
"date_sent": "Tue, 19 Aug 2026 00:00:00 +0000"}
|
|
|
|
|
|
def test_an_accepted_message_the_carrier_refused_is_a_failure_not_a_success():
|
|
"""The whole point. Twilio said 201 at send time for every one of these."""
|
|
report = reconcile([
|
|
Message("SM1", "+15550001111", "delivered", None, None),
|
|
Message("SM2", "+15550002222", "undelivered", "30032", None),
|
|
Message("SM3", "+15550003333", "failed", "30006", None),
|
|
])
|
|
assert report.delivered == 1
|
|
assert [m.sid for m in report.failed] == ["SM2", "SM3"]
|
|
assert report.ok is False
|
|
|
|
|
|
def test_sent_is_not_delivered():
|
|
"""Twilio's `sent` means it left Twilio, not that a handset received it —
|
|
counting it as success is the same mistake one layer down."""
|
|
report = reconcile([Message("SM1", "+15550001111", "sent", None, None)])
|
|
assert report.delivered == 0
|
|
assert report.in_flight == 1
|
|
assert report.ok is True # not yet a failure, but not a delivery either
|
|
|
|
|
|
def test_a_clean_window_is_ok():
|
|
report = reconcile([Message("SM1", "+1555", "delivered", None, None)])
|
|
assert report.ok and not report.failed
|
|
|
|
|
|
def test_an_unrecognised_status_is_surfaced_rather_than_assumed_good():
|
|
report = reconcile([Message("SM1", "+1555", "carrier_shrugged", None, None)])
|
|
assert report.unknown == ["carrier_shrugged"]
|
|
assert report.delivered == 0
|
|
|
|
|
|
def test_the_report_groups_by_reason_and_never_prints_a_full_number():
|
|
report = reconcile([
|
|
Message("SM1", "+17066762576", "undelivered", "30032", None),
|
|
Message("SM2", "+17066762576", "undelivered", "30032", None),
|
|
Message("SM3", "+15551230000", "failed", "30006", None),
|
|
])
|
|
text = describe(report)
|
|
assert "2x error 30032" in text.replace(" ", " ")
|
|
assert "***2576" in text
|
|
assert "+17066762576" not in text, "a report that leaks full numbers cannot be pasted anywhere"
|
|
assert "unverified toll-free" in text
|
|
|
|
|
|
def test_the_ledger_query_reads_every_sender_not_just_ours():
|
|
"""Reconciling against our own record would have missed the failures that
|
|
started this, because a different service sent them."""
|
|
captured = {}
|
|
|
|
def fake_get(url, *, headers, timeout):
|
|
captured["url"] = url
|
|
return ledger(row(sid="SMx", status="undelivered", error="30032"))
|
|
|
|
found = fetch(account_sid="AC123", auth_token="secret",
|
|
since=datetime(2026, 8, 12, tzinfo=timezone.utc), get=fake_get)
|
|
|
|
assert "Messages.json" in captured["url"]
|
|
assert "2026-08-12" in captured["url"]
|
|
assert "secret" not in captured["url"], "credentials belong in the header, not the query"
|
|
assert [m.status for m in found] == ["undelivered"]
|
|
assert found[0].error_code == "30032"
|
|
|
|
|
|
def test_a_refused_ledger_query_is_loud():
|
|
def refuse(url, *, headers, timeout):
|
|
return Response(status=401, body="unauthorized", headers={})
|
|
|
|
with pytest.raises(RuntimeError):
|
|
fetch(account_sid="AC1", auth_token="bad", since=window_start(24), get=refuse)
|
|
|
|
|
|
def test_window_start_looks_backwards():
|
|
assert window_start(24) < datetime.now(timezone.utc)
|
|
|
|
|
|
# --------------------------------------------------------------- email
|
|
|
|
from app.reconcile import EmailReport, describe_email, email_bounces # noqa: E402
|
|
|
|
|
|
def test_a_key_that_cannot_read_bounces_says_so_instead_of_reporting_clean():
|
|
"""The estate's key is send-only. Skipping the check quietly is how a
|
|
channel nobody can see becomes a channel nobody checks."""
|
|
def forbidden(url, *, headers, timeout):
|
|
return Response(status=403, body='{"errors":[{"message":"access forbidden"}]}', headers={})
|
|
|
|
report = email_bounces(api_key="SG.x", since=window_start(24), get=forbidden)
|
|
|
|
assert report.checked is False
|
|
assert report.bounces == []
|
|
assert "suppression.read" in report.reason
|
|
assert "NOT VERIFIED" in describe_email(report)
|
|
|
|
|
|
def test_bounces_are_reported_without_publishing_addresses():
|
|
def ledger_of_bounces(url, *, headers, timeout):
|
|
return Response(status=200, headers={}, body=json.dumps([
|
|
{"email": "[email protected]", "reason": "550 5.1.1 user unknown"}]))
|
|
|
|
report = email_bounces(api_key="SG.x", since=window_start(24), get=ledger_of_bounces)
|
|
text = describe_email(report)
|
|
|
|
assert report.checked and len(report.bounces) == 1
|
|
assert "***@example.test" in text
|
|
assert "[email protected]" not in text
|
|
|
|
|
|
def test_no_key_is_reported_as_unverified_not_as_healthy():
|
|
report = email_bounces(api_key="", since=window_start(24))
|
|
assert report.checked is False and "no SendGrid key" in report.reason
|