r""" auth/twofactor.py `handle_2fa()` — an approval the login page never picks up. Live 2026-10-29, three logins in a row: the push mail came 6 to 8.5 minutes late, the bot opened its link and clicked "Yes, it's me", and the login page stayed on "eClinicalWorks sent a notification to your email" with Resend OTP still there. The bot waited out LOGIN_TIMEOUT and gave up ("Login not confirmed"). Now it presses Resend, waits for the new mail and approves that one, up to OTP_PICKUP_RESENDS times (operator, 2026-10-30: no manual step). python tests\test_twofa_late_approval.py Local headless chromium, `page.set_content()` only. The mailbox and the approval tab are stand-ins: no eCW, no mail. """ import sys import pathlib import tempfile ROOT = pathlib.Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "src")) from ecw_bot import settings as cfg # noqa: E402 from ecw_bot.auth import twofactor # noqa: E402 from ecw_bot.auth.mailbox import Challenge # noqa: E402 CASES = [] def case(fn): CASES.append(fn) return fn # The push screen as seen live: the notice in #dispMessage, Resend enabled. _PAGE = """
eClinicalWorks sent a notification to your email. Please confirm to login.
""" def _go_home(page): page.evaluate("document.getElementById('jellybean-panelLink4')" ".style.display = ''; document.getElementById('dispMessage')" ".style.display = 'none'") def _fakes(page, picked_up_on): """Mailbox hands out links in order; approval number `picked_up_on` (1-based) is the one the page takes.""" seen = {"approvals": 0, "mails": 0} def get_challenge(**kw): seen["mails"] += 1 return Challenge("link", f"https://example.test/approve/{seen['mails']}") def approve(context, url): seen["approvals"] += 1 if seen["approvals"] == picked_up_on: _go_home(page) return True twofactor.mailbox.get_challenge = get_challenge twofactor._approve_in_new_tab = approve return seen @case def an_approval_not_picked_up_is_resent_and_approved_again(page): page.set_content(_PAGE) seen = _fakes(page, picked_up_on=2) twofactor.handle_2fa(page, None, "Test", None) assert seen["approvals"] == 2, seen assert page.evaluate("window.resends") == 1 assert page.locator("#jellybean-panelLink4").is_visible() @case def an_approval_picked_up_at_once_needs_no_resend(page): page.set_content(_PAGE) seen = _fakes(page, picked_up_on=1) twofactor.handle_2fa(page, None, "Test", None) assert seen["approvals"] == 1, seen assert page.evaluate("window.resends || 0") == 0 @case def resends_stop_at_the_limit(page): page.set_content(_PAGE) seen = _fakes(page, picked_up_on=99) twofactor.handle_2fa(page, None, "Test", None) assert page.evaluate("window.resends") == cfg.OTP_PICKUP_RESENDS assert seen["approvals"] == cfg.OTP_PICKUP_RESENDS + 1, seen def main() -> int: from playwright.sync_api import sync_playwright failures = 0 real = (twofactor.mailbox.get_challenge, twofactor._approve_in_new_tab) with tempfile.TemporaryDirectory() as tmp: cfg.SCREENSHOTS = pathlib.Path(tmp) # never the real folder cfg.ECW_SECRET_KEY = "" cfg.OTP_PICKUP_SECONDS = 2 with sync_playwright() as pw: browser = pw.chromium.launch(headless=True) page = browser.new_page() for fn in CASES: name = fn.__name__.replace("_", " ") try: fn(page) 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: (twofactor.mailbox.get_challenge, twofactor._approve_in_new_tab) = real browser.close() print(f"\n{len(CASES) - failures}/{len(CASES)} passed") return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())