Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a119cf-56d0-7fa3-840b-eb8732e1db0b
414 lines
16 KiB
Python
414 lines
16 KiB
Python
"""Admitted caller contract: only synthetic tokens, providers, recipients and ledgers."""
|
|
|
|
import io
|
|
import json
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
import pytest
|
|
from email_connect.callers import CallerPolicy, ScopedTransactionalApplication, load_callers
|
|
from email_connect.transactional import ProviderError, SQLiteDeliveryStore, TransactionalApplication
|
|
|
|
|
|
class Provider:
|
|
def __init__(self):
|
|
self.calls = []
|
|
|
|
def send(self, *args):
|
|
self.calls.append(args)
|
|
return "synthetic-reference-recipient=member@example.test-token=must-not-leak"
|
|
|
|
def check(self):
|
|
return None
|
|
|
|
|
|
def policy(name="caller-a", **changes):
|
|
values = dict(
|
|
caller_id=name,
|
|
token="synthetic-token-" + name,
|
|
source=name,
|
|
bindings=frozenset({("tenant:a", "client:a")}),
|
|
operations=frozenset({"send", "delivery-status", "verify", "consume", "cancel"}),
|
|
templates=frozenset({"family_member.invited"}),
|
|
rate_per_minute=60,
|
|
burst=100,
|
|
max_payload_bytes=32768,
|
|
)
|
|
values.update(changes)
|
|
return CallerPolicy(**values)
|
|
|
|
|
|
def event(caller="caller-a", **changes):
|
|
payload = {
|
|
"id": "evt-1",
|
|
"type": "family_member.invited",
|
|
"source": caller,
|
|
"tenant": "tenant:a",
|
|
"client_id": "client:a",
|
|
"data": {"primary_email": "member@example.test", "invitation_id": "invite-1"},
|
|
}
|
|
payload.update(changes)
|
|
return payload
|
|
|
|
|
|
def verification(**changes):
|
|
result = {
|
|
"registration_id": "reg-1",
|
|
"normalized_email": "member@example.test",
|
|
"preferred_username": "member",
|
|
"tenant": "tenant:a",
|
|
"client_id": "client:a",
|
|
"correlation_id": "trace-1",
|
|
}
|
|
result.update(changes)
|
|
return result
|
|
|
|
|
|
def invoke(app, payload, caller="caller-a", path="/v1/send", token=None, key=None):
|
|
raw = json.dumps(payload).encode()
|
|
response = {}
|
|
env = {
|
|
"PATH_INFO": path,
|
|
"REQUEST_METHOD": "POST",
|
|
"CONTENT_LENGTH": str(len(raw)),
|
|
"wsgi.input": io.BytesIO(raw),
|
|
"HTTP_AUTHORIZATION": "Bearer " + (token or "synthetic-token-" + caller),
|
|
"HTTP_IDEMPOTENCY_KEY": key if key is not None else payload.get("id", ""),
|
|
}
|
|
body = b"".join(
|
|
app(
|
|
env,
|
|
lambda status, headers: response.update(
|
|
status=int(status.split()[0]),
|
|
headers=dict(headers),
|
|
),
|
|
)
|
|
)
|
|
return response["status"], json.loads(body), response["headers"]
|
|
|
|
|
|
def application(tmp_path, policies=None, clock=None):
|
|
store = SQLiteDeliveryStore(str(tmp_path / "ledger.db"))
|
|
provider = Provider()
|
|
app = ScopedTransactionalApplication(
|
|
store,
|
|
provider,
|
|
"https://issuer.example.test",
|
|
policies or [policy(), policy("caller-b")],
|
|
**({"clock": clock} if clock is not None else {}),
|
|
)
|
|
return app, store, provider
|
|
|
|
|
|
def test_credentials_determine_caller_and_status_is_scoped_and_redacted(tmp_path):
|
|
app, _, provider = application(tmp_path)
|
|
assert invoke(app, event(), token="wrong-credential")[0] == 401
|
|
assert invoke(app, event("caller-b"))[0] == 400
|
|
accepted, body, headers = invoke(app, event())
|
|
assert accepted == 202 and body["event_id"] == "evt-1" and body["reference"].startswith("dvr_")
|
|
assert headers["Cache-Control"] == "private, no-store"
|
|
status, record, _ = invoke(app, {"event_id": "evt-1"}, path="/v1/delivery-status")
|
|
assert status == 200 and record["state"] == "provider_accepted"
|
|
assert record["reference"] == body["reference"]
|
|
_, other, _ = invoke(app, {"event_id": "evt-1"}, caller="caller-b", path="/v1/delivery-status")
|
|
assert other["state"] == "not_found"
|
|
assert len(provider.calls) == 1
|
|
serialized = json.dumps([body, record, other])
|
|
assert "member@example.test" not in serialized and "must-not-leak" not in serialized
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"change",
|
|
[
|
|
{"tenant": "tenant:b"},
|
|
{"client_id": "client:b"},
|
|
{"tenant": ["tenant:a"]},
|
|
{"type": "unapproved-template"},
|
|
{"type": []},
|
|
{"source": "user-engine"},
|
|
],
|
|
)
|
|
def test_scope_and_template_denials_never_submit(tmp_path, change):
|
|
app, _, provider = application(tmp_path)
|
|
assert invoke(app, event(**change))[0] == 400
|
|
assert provider.calls == []
|
|
|
|
|
|
def test_retry_and_payload_identity_are_independent_across_callers_and_restart(tmp_path):
|
|
app, store, provider = application(tmp_path)
|
|
first = invoke(app, event())[1]
|
|
assert invoke(app, event())[1]["status"] == "duplicate"
|
|
assert (
|
|
invoke(
|
|
app, event(data={"primary_email": "other@example.test", "invitation_id": "invite-1"})
|
|
)[0]
|
|
== 400
|
|
)
|
|
assert invoke(app, event("caller-b"), caller="caller-b")[0] == 202
|
|
assert len(provider.calls) == 2
|
|
store.db.close()
|
|
restarted, _, empty_provider = application(tmp_path)
|
|
assert invoke(restarted, event())[1]["reference"] == first["reference"]
|
|
assert empty_provider.calls == []
|
|
|
|
|
|
def test_unknown_submission_survives_restart_without_resend(tmp_path):
|
|
app, store, provider = application(tmp_path)
|
|
|
|
def unknown(*args):
|
|
provider.calls.append(args)
|
|
raise OSError("synthetic SMTP recipient/token must not leak")
|
|
|
|
provider.send = unknown
|
|
status, body, _ = invoke(app, event())
|
|
assert status == 422 and body == {"error": "delivery_outcome_unknown", "retryable": False}
|
|
store.db.close()
|
|
restarted, _, next_provider = application(tmp_path)
|
|
assert invoke(restarted, event())[1] == body
|
|
assert len(provider.calls) == 1 and next_provider.calls == []
|
|
|
|
|
|
def test_rate_budget_survives_restart_and_backward_clock_and_is_per_caller(tmp_path):
|
|
now = [100.0]
|
|
policies = [policy(burst=1, rate_per_minute=2), policy("caller-b", burst=1, rate_per_minute=2)]
|
|
app, store, provider = application(tmp_path, policies, clock=lambda: now[0])
|
|
assert invoke(app, event())[0] == 202
|
|
status, _, headers = invoke(app, event(id="evt-2"))
|
|
assert status == 429 and headers["Retry-After"] == "30" and len(provider.calls) == 1
|
|
assert invoke(app, event("caller-b"), caller="caller-b")[0] == 202
|
|
store.db.close()
|
|
restarted, _, next_provider = application(tmp_path, policies, clock=lambda: now[0])
|
|
assert invoke(restarted, event(id="evt-2"))[0] == 429
|
|
now[0] = 90
|
|
assert invoke(restarted, event(id="evt-2"))[0] == 429
|
|
now[0] = 130
|
|
assert invoke(restarted, event(id="evt-2"))[0] == 202
|
|
assert len(next_provider.calls) == 1
|
|
|
|
|
|
def test_concurrent_connections_cannot_exceed_request_burst(tmp_path):
|
|
first, store, provider = application(tmp_path, [policy(burst=1)])
|
|
second = ScopedTransactionalApplication(
|
|
SQLiteDeliveryStore(str(tmp_path / "ledger.db")),
|
|
provider,
|
|
"https://issuer.example.test",
|
|
[policy(burst=1)],
|
|
)
|
|
with ThreadPoolExecutor(max_workers=2) as workers:
|
|
responses = list(workers.map(lambda app: invoke(app, event()), [first, second]))
|
|
assert sorted(r[0] for r in responses) == [202, 429]
|
|
assert len(provider.calls) == 1
|
|
|
|
|
|
def test_verification_owner_and_binding_checked_before_consumption_or_cancellation(tmp_path):
|
|
app, store, provider = application(tmp_path)
|
|
request = verification()
|
|
first = invoke(app, request, path="/v1/registration-verifications")[1]
|
|
assert invoke(app, request, path="/v1/registration-verifications")[1] == first
|
|
handle = provider.calls[0][2].split("handle=", 1)[1].splitlines()[0]
|
|
for path in ("/consume", "/cancel"):
|
|
assert (
|
|
invoke(
|
|
app,
|
|
{"handle": handle},
|
|
caller="caller-b",
|
|
path="/v1/registration-verifications" + path,
|
|
)[0]
|
|
== 400
|
|
)
|
|
assert invoke(app, request, caller="caller-b", path="/v1/registration-verifications")[0] == 202
|
|
assert len(provider.calls) == 2
|
|
legacy = TransactionalApplication(store, provider, "legacy", "https://issuer.example.test")
|
|
raw = json.dumps({"handle": handle}).encode()
|
|
response = {}
|
|
legacy(
|
|
{
|
|
"PATH_INFO": "/v1/registration-verifications/consume",
|
|
"REQUEST_METHOD": "POST",
|
|
"CONTENT_LENGTH": str(len(raw)),
|
|
"wsgi.input": io.BytesIO(raw),
|
|
"HTTP_AUTHORIZATION": "Bearer legacy",
|
|
},
|
|
lambda status, headers: response.update(status=int(status.split()[0])),
|
|
)
|
|
assert response["status"] == 400
|
|
limited = ScopedTransactionalApplication(
|
|
store,
|
|
provider,
|
|
"https://issuer.example.test",
|
|
[policy(bindings=frozenset({("tenant:b", "client:b")}))],
|
|
)
|
|
assert (
|
|
invoke(limited, {"handle": handle}, path="/v1/registration-verifications/consume")[0] == 400
|
|
)
|
|
assert invoke(app, {"handle": handle}, path="/v1/registration-verifications/consume")[0] == 200
|
|
assert invoke(app, {"handle": handle}, path="/v1/registration-verifications/cancel")[0] == 400
|
|
|
|
|
|
def test_suppression_and_unsuppression_require_explicit_owner_operations(tmp_path):
|
|
ops = policy("mail-operator", operations=frozenset({"suppress", "unsuppress"}))
|
|
app, _, provider = application(tmp_path, [policy(), ops])
|
|
payload = {"recipient": "member@example.test", "reason": "complaint"}
|
|
assert invoke(app, payload, path="/v1/suppressions")[0] == 403
|
|
assert invoke(app, payload, caller="mail-operator", path="/v1/suppressions")[0] == 200
|
|
assert invoke(app, event())[1]["error"] == "recipient_suppressed"
|
|
assert not provider.calls
|
|
assert invoke(app, payload, caller="mail-operator", path="/v1/suppressions/remove")[0] == 200
|
|
assert invoke(app, event())[0] == 202
|
|
|
|
|
|
def test_original_caller_legacy_reservations_and_links_need_explicit_adoption(tmp_path):
|
|
app, store, provider = application(tmp_path)
|
|
payload = verification()
|
|
legacy = TransactionalApplication(store, provider, "legacy", "https://issuer.example.test")
|
|
raw = json.dumps(payload).encode()
|
|
result = {}
|
|
legacy(
|
|
{
|
|
"PATH_INFO": "/v1/registration-verifications",
|
|
"REQUEST_METHOD": "POST",
|
|
"CONTENT_LENGTH": str(len(raw)),
|
|
"wsgi.input": io.BytesIO(raw),
|
|
"HTTP_AUTHORIZATION": "Bearer legacy",
|
|
},
|
|
lambda status, headers: result.update(status=status),
|
|
)
|
|
handle = provider.calls[0][2].split("handle=", 1)[1].splitlines()[0]
|
|
adopter = policy("user-engine", adopt_legacy_verifications=True)
|
|
adopted = ScopedTransactionalApplication(
|
|
store, provider, "https://issuer.example.test", [adopter]
|
|
)
|
|
assert (
|
|
invoke(adopted, payload, caller="user-engine", path="/v1/registration-verifications")[0]
|
|
== 202
|
|
)
|
|
assert len(provider.calls) == 1
|
|
assert (
|
|
invoke(
|
|
adopted,
|
|
{"handle": handle},
|
|
caller="user-engine",
|
|
path="/v1/registration-verifications/consume",
|
|
)[0]
|
|
== 200
|
|
)
|
|
with pytest.raises(ValueError, match="original_caller"):
|
|
policy(adopt_legacy_verifications=True)
|
|
|
|
|
|
def test_payload_limit_unknown_operation_and_provider_status_have_no_implicit_grant(tmp_path):
|
|
app, _, provider = application(tmp_path, [policy(max_payload_bytes=100)])
|
|
assert invoke(app, event(data={"primary_email": "x" * 1000}))[0] == 413
|
|
assert invoke(app, {}, path="/v1/status")[0] == 403
|
|
assert invoke(app, {}, path="/unknown")[0] == 404
|
|
assert not provider.calls
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"change",
|
|
[
|
|
{"burst": 0},
|
|
{"rate_per_minute": True},
|
|
{"max_payload_bytes": 200000},
|
|
{"token": ""},
|
|
{"bindings": frozenset()},
|
|
{"templates": frozenset({"arbitrary-body"})},
|
|
],
|
|
)
|
|
def test_policy_refuses_unbounded_or_unauthorized_configuration(change):
|
|
with pytest.raises(ValueError):
|
|
policy(**change)
|
|
|
|
|
|
def test_policy_loader_uses_independent_files_and_never_echoes_credentials(tmp_path):
|
|
token_file = tmp_path / "token"
|
|
token_file.write_text("synthetic-token-caller-a\n")
|
|
entry = dict(
|
|
caller_id="caller-a",
|
|
source="caller-a",
|
|
bindings=[["tenant:a", "client:a"]],
|
|
operations=["send"],
|
|
templates=["family_member.invited"],
|
|
rate_per_minute=10,
|
|
burst=3,
|
|
max_payload_bytes=8192,
|
|
token_file=str(token_file),
|
|
)
|
|
config = tmp_path / "policy.json"
|
|
config.write_text(json.dumps({"schema_version": 1, "callers": [entry]}))
|
|
loaded = load_callers(config)
|
|
assert loaded[0].token == "synthetic-token-caller-a"
|
|
assert "synthetic-token" not in repr(loaded)
|
|
config.write_text(
|
|
json.dumps({"schema_version": 1, "callers": [entry, dict(entry, caller_id="caller-b")]})
|
|
)
|
|
with pytest.raises(ValueError, match="caller_configuration_invalid") as error:
|
|
load_callers(config)
|
|
assert "synthetic-token" not in str(error.value)
|
|
entry["token"] = "synthetic-embedded-secret"
|
|
config.write_text(json.dumps({"schema_version": 1, "callers": [entry]}))
|
|
with pytest.raises(ValueError, match="caller_configuration_invalid"):
|
|
load_callers(config)
|
|
|
|
|
|
@pytest.mark.parametrize("retryable", [False, True])
|
|
def test_definitive_failure_retains_retry_policy_after_restart(tmp_path, retryable):
|
|
app, store, provider = application(tmp_path)
|
|
|
|
def reject(*args):
|
|
provider.calls.append(args)
|
|
raise ProviderError(
|
|
"temporary_deferral" if retryable else "permanent_rejection", retryable=retryable
|
|
)
|
|
|
|
provider.send = reject
|
|
first = invoke(app, event())
|
|
assert first[0] == (503 if retryable else 422)
|
|
store.db.close()
|
|
restarted, _, next_provider = application(tmp_path)
|
|
result = invoke(restarted, event())
|
|
assert result[0] == (202 if retryable else 422)
|
|
assert len(next_provider.calls) == int(retryable)
|
|
|
|
|
|
def test_legacy_invitation_adoption_preserves_accepted_receipt(tmp_path):
|
|
store = SQLiteDeliveryStore(str(tmp_path / "ledger.db"))
|
|
provider = Provider()
|
|
legacy = TransactionalApplication(
|
|
store, provider, "legacy-token", "https://issuer.example.test"
|
|
)
|
|
old = event("user-engine")
|
|
old.pop("tenant")
|
|
old.pop("client_id")
|
|
assert invoke(legacy, old, token="legacy-token")[0] == 202
|
|
admitted = policy("user-engine", legacy_invitation_binding=("tenant:a", "client:a"))
|
|
scoped = ScopedTransactionalApplication(
|
|
store, provider, "https://issuer.example.test", [admitted]
|
|
)
|
|
assert invoke(scoped, old, caller="user-engine")[1]["status"] == "duplicate"
|
|
assert len(provider.calls) == 1
|
|
assert invoke(scoped, dict(old, id="ec2_reserved"), caller="user-engine")[0] == 400
|
|
|
|
|
|
def test_historical_failed_reservation_is_conservatively_terminal(tmp_path):
|
|
import sqlite3
|
|
|
|
path = tmp_path / "old.db"
|
|
with sqlite3.connect(path) as db:
|
|
db.execute(
|
|
"CREATE TABLE send_attempts (event_id TEXT PRIMARY KEY, "
|
|
"fingerprint TEXT NOT NULL, state TEXT NOT NULL)"
|
|
)
|
|
db.execute("INSERT INTO send_attempts VALUES ('old-event','old-fingerprint','failed')")
|
|
store = SQLiteDeliveryStore(str(path))
|
|
with pytest.raises(ProviderError, match="delivery_outcome_unknown"):
|
|
store.reserve_send("old-event", "old-fingerprint")
|
|
|
|
|
|
def test_invalid_policy_refuses_startup_before_database_mutation(tmp_path, monkeypatch):
|
|
from email_connect.transactional import main
|
|
|
|
monkeypatch.setenv("EMAIL_CONNECT_CALLERS_FILE", str(tmp_path / "missing.json"))
|
|
monkeypatch.setenv("EMAIL_CONNECT_DATABASE_PATH", str(tmp_path / "not-created.db"))
|
|
with pytest.raises(ValueError, match="caller_configuration_invalid"):
|
|
main()
|
|
assert not (tmp_path / "not-created.db").exists()
|