"""A minimal outside master (CMS / ticketing stand-in) for email in the slave role. It records every message email sends it, issues case identifiers, and drives email through the contract: assign, policy, mail.send, cancel. Nothing happens on its own: `pump()` performs queued auto-assignments, so tests stay deterministic and no handler calls back into email. master = StubMaster({"email": key}, email_url="http://127.0.0.1:PORT/master", auto_assign=True) base = serve(master.server()) # POST /events (signed by email) """ from __future__ import annotations import itertools import threading from typing import Optional from .. import message_errors from ..contract import Idempotent, json_server, message, post, refuse class StubMaster: def __init__(self, keys: dict[str, bytes], email_url: Optional[str] = None, auto_assign: bool = False, id_prefix: str = "EXT-", peer_id: str = "cms-1", email_peer: str = "email"): self.keys, self.peer_id, self.email_peer = keys, peer_id, email_peer self.email_url = email_url self.auto_assign = auto_assign self.ids = (f"{id_prefix}{n}" for n in itertools.count(100)) self.received: list[dict] = [] self.pending_intakes: list[str] = [] self.lock = threading.Lock() self._once = Idempotent() def receive(self, msg: dict, peer: Optional[str] = None, rest: str = "") -> tuple[int, dict]: if peer != self.email_peer: return refuse("wrong-peer", "events come from email only", 403) return self._once(msg, self._receive) def _receive(self, msg): errors = message_errors(msg) if errors: return refuse("schema", "; ".join(errors), 400) with self.lock: self.received.append(msg) if msg["type"] == "case.intake": self.pending_intakes.append(msg["intake_ref"]) return 200, {"ok": True} def of_type(self, type_: str) -> list[dict]: return [m for m in self.received if m["type"] == type_] # -- commands to email ------------------------------------------------------- def command(self, type_: str, case: Optional[dict] = None, **fields) -> tuple[Optional[int], object]: return post(self.email_url, message(type_, case, **fields), (self.peer_id, self.keys[self.email_peer])) def pump(self) -> list[tuple]: """Assign case ids to queued intakes (when auto_assign is on).""" out = [] while self.auto_assign: with self.lock: if not self.pending_intakes: break ref = self.pending_intakes.pop(0) out.append(self.assign(ref, next(self.ids))) return out def assign(self, intake_ref: str, resource_id: str): return self.command("case.assign", intake_ref=intake_ref, resource_id=resource_id) def policy(self, case: dict, autonomous_send: Optional[bool] = None, transfers=None): return self.command("case.policy", case, autonomous_send=autonomous_send, transfers=transfers) def send(self, case: dict, in_reply_to: str, body: str, subject: Optional[str] = None): return self.command("mail.send", case, in_reply_to=in_reply_to, body=body, subject=subject) def cancel(self, case: dict, handoff_id: str, reason: str = "timed-out"): """The master owns timeouts in the slave role: it ends a handoff itself.""" return self.command("handoff.cancel", case, handoff_id=handoff_id, reason=reason) def server(self, host="127.0.0.1", port=0): return json_server({"/events": self.receive}, self.keys, host=host, port=port)