Self-healing agent (supervisor, recovery, login fixes, operator mail) and Portal sage redesign
Snapshot of D:\Ecw Process a11c9ff (2026-10-30). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fafa5285eb
commit
d500f750da
26 files changed
+1704
-357
No files matched your search
@@ -0,0 +1,134 @@
|
||||
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 = """
|
||||
<div id="dispMessage">eClinicalWorks sent a notification to your email.
|
||||
Please confirm to login.</div>
|
||||
<input type="button" id="resendOtpBtn" value="Resend OTP"
|
||||
onclick="window.resends = (window.resends || 0) + 1">
|
||||
<a id="jellybean-panelLink4" style="display:none">Home</a>
|
||||
<script>window.resends = 0</script>
|
||||
"""
|
||||
|
||||
|
||||
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())
|
||||
Reference in new issue
Block a user