r""" core/notify.py — the agent's email to the operator (operator, 2026-10-30): what happened, what the agent did about it, and whether it recovered. Rules the operator set: * sent from the bot's own mailbox (SMTP, the 2FA mailbox's account); * screenshots of login and error pages only — never a claim screen, never page HTML: those carry PHI; * no claim numbers in the mail. python tests\test_notify.py No mail is sent: the SMTP transport is a stand-in. """ import sys import pathlib import tempfile sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "src")) from ecw_bot import settings as cfg # noqa: E402 from ecw_bot.core import notify # noqa: E402 CASES = [] def case(fn): CASES.append(fn) return fn def _capture(): sent = [] notify._transport = lambda msg: sent.append(msg) return sent def _files(tmp, *names): out = [] for n in names: p = pathlib.Path(tmp) / n p.write_bytes(b"x") out.append(p) return out @case def an_incident_says_what_happened_and_what_the_agent_did(tmp): sent = _capture() assert notify.incident("Login failed", "2FA mail came 7 minutes late", "Resent the OTP, will retry in 10 minutes", recovered=False) is True msg = sent[0] assert msg["To"] == cfg.NOTIFY_EMAIL_TO assert "Login failed" in msg["Subject"] body = msg.get_body(("plain",)).get_content() assert "2FA mail came 7 minutes late" in body assert "Resent the OTP" in body assert "Not recovered" in body @case def claim_numbers_never_reach_the_mail(tmp): sent = _capture() notify.incident("Stuck dialog on claim 9107000", "Claim 9107000 form would not close", "Reloaded eCW", recovered=True) msg = sent[0] text = msg["Subject"] + msg.get_body(("plain",)).get_content() assert "9107000" not in text, text @case def only_login_and_error_screenshots_are_attached(tmp): sent = _capture() shots = _files(tmp, "211944_login_unconfirmed.png", "9107000_claim_form_leftover_204500.png", "211944_login_unconfirmed.html", "101500_security_image_page.png") notify.incident("Login failed", "x", "y", recovered=False, shots=shots) names = sorted(a.get_filename() for a in sent[0].iter_attachments()) assert names == ["101500_security_image_page.png", "211944_login_unconfirmed.png"], names @case def nothing_is_sent_when_no_recipient_is_set(tmp): sent = _capture() old = cfg.NOTIFY_EMAIL_TO cfg.NOTIFY_EMAIL_TO = "" try: assert notify.incident("x", "y", "z", recovered=True) is False finally: cfg.NOTIFY_EMAIL_TO = old assert sent == [] @case def a_mail_server_error_never_stops_the_run(tmp): def boom(msg): raise OSError("connection refused") notify._transport = boom assert notify.incident("x", "y", "z", recovered=True) is False def main() -> int: failures = 0 real = notify._transport cfg.NOTIFY_EMAIL_TO = "operator@example.test" for fn in CASES: name = fn.__name__.replace("_", " ") with tempfile.TemporaryDirectory() as tmp: try: fn(tmp) print(f" ok {name}") except AssertionError as exc: print(f" FAIL {name}: {exc}") failures += 1 except Exception as exc: print(f" ERROR {name}: {type(exc).__name__}: {exc}") failures += 1 finally: notify._transport = real print(f"\n{len(CASES) - failures}/{len(CASES)} passed") return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())