email-connect/tests/test_transactional.py
tegwick 752d91cf5c
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Add single-use registration verification
2026-08-10 12:29:55 +02:00

38 lines
3.2 KiB
Python

import io, json
from email_connect.transactional import SQLiteDeliveryStore, TransactionalApplication
class Provider:
def __init__(self): self.calls=[]
def send(self,*args): self.calls.append(args); return "message:1"
def event(): return {"id":"evt-1","type":"family_member.invited","source":"user-engine","data":{"primary_email":"person@example.test","invitation_id":"inv-1"}}
def invoke(app,payload,token="opaque",key="evt-1"):
raw=json.dumps(payload).encode(); result={}; body=b"".join(app({"PATH_INFO":"/v1/send","REQUEST_METHOD":"POST","CONTENT_LENGTH":str(len(raw)),"wsgi.input":io.BytesIO(raw),"HTTP_AUTHORIZATION":f"Bearer {token}","HTTP_IDEMPOTENCY_KEY":key},lambda s,h:result.update(status=s))); return result["status"],json.loads(body)
def test_sends_fixed_template_once(tmp_path):
provider=Provider(); app=TransactionalApplication(SQLiteDeliveryStore(str(tmp_path/"mail.db")),provider,"opaque","https://users.example")
assert invoke(app,event())[0].startswith("202")
assert invoke(app,event())[1]["status"] == "duplicate"
assert len(provider.calls)==1 and "/invitations/inv-1" in provider.calls[0][2]
def test_rejects_auth_recipient_template_and_key(tmp_path):
app=TransactionalApplication(SQLiteDeliveryStore(str(tmp_path/"mail.db")),Provider(),"opaque","https://users.example")
assert invoke(app,event(),token="bad")[0].startswith("401")
bad=event(); bad["data"]["primary_email"]="bad\n@example.test"; assert invoke(app,bad)[1]["error"]=="invalid_recipient"
bad=event(); bad["type"]="arbitrary.send"; assert invoke(app,bad)[1]["error"]=="template_not_allowed"
assert invoke(app,event(),key="other")[1]["error"]=="idempotency_key_mismatch"
def test_registration_verification_is_digest_only_and_single_use(tmp_path):
provider=Provider(); store=SQLiteDeliveryStore(str(tmp_path/"mail.db")); app=TransactionalApplication(store,provider,"opaque","https://users.example")
request={"registration_id":"reg-1","normalized_email":"person@example.test","preferred_username":"person","client_id":"coulomb-social","tenant":"tenant:coulomb","correlation_id":"corr-1"}
raw=json.dumps(request).encode(); result={}; response=json.loads(b"".join(app({"PATH_INFO":"/v1/registration-verifications","REQUEST_METHOD":"POST","CONTENT_LENGTH":str(len(raw)),"wsgi.input":io.BytesIO(raw),"HTTP_AUTHORIZATION":"Bearer opaque"},lambda s,h:result.update(status=s))))
assert result["status"].startswith("202") and response["accepted"]
text=provider.calls[0][2]; handle=text.split("handle=",1)[1].splitlines()[0]
stored=store.db.execute("SELECT handle_hash FROM verifications").fetchone()[0]
assert handle not in stored and stored
consume=json.dumps({"handle":handle}).encode(); env={"PATH_INFO":"/v1/registration-verifications/consume","REQUEST_METHOD":"POST","CONTENT_LENGTH":str(len(consume)),"wsgi.input":io.BytesIO(consume),"HTTP_AUTHORIZATION":"Bearer opaque"}
evidence=json.loads(b"".join(app(env,lambda s,h:result.update(status=s))))
assert evidence["purpose"]=="public-registration" and evidence["email"]=="person@example.test"
env["wsgi.input"]=io.BytesIO(consume)
replay=json.loads(b"".join(app(env,lambda s,h:result.update(status=s))))
assert replay["error"]=="verification_invalid"