Files
2026-10-30 10:58:35 +05:30

131 lines
3.8 KiB
Python

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())