Initial import: eCW Charge Validation bot and results Portal
Snapshot of the working project as of 2026-10-23 (local 2d4d4fc), one commit with no earlier history: the tracked files were de-identified first (synthetic claim ids and patient data only; see Context.md section 7). Credentials, run logs, screenshots, claim reports and client workbooks are gitignored and not included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
ba15a574ab
205 files changed
+56772
No files matched your search
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
2FA dead-end detection.
|
||||
|
||||
python tests\\test_dead_end.py
|
||||
|
||||
The `#dispMessage` strings below are copied from the real page eCW served on
|
||||
2026-09-07, not invented. That matters: all four states — the healthy one and
|
||||
the three dead ends — are written into the **same element**, so the only thing
|
||||
separating "still waiting for mail" from "this page is finished" is the text.
|
||||
Get the matching wrong in either direction and the cost is real: a missed dead
|
||||
end burns twenty minutes and an approval link, while a false positive throws
|
||||
away a login that was about to succeed.
|
||||
|
||||
No browser is started. A stub with one method is all `reason()` touches, which
|
||||
is the point of keeping it that small.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import pathlib
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "src"))
|
||||
|
||||
from ecw_bot.auth import dead_end # noqa: E402
|
||||
from ecw_bot.auth.dead_end import LoginTimedOut # noqa: E402
|
||||
|
||||
|
||||
class FakePage:
|
||||
"""Just enough Page for `reason()`: locator().first.inner_text()."""
|
||||
|
||||
def __init__(self, text, raises=False):
|
||||
self._text = text
|
||||
self._raises = raises
|
||||
|
||||
def locator(self, _selector):
|
||||
return self
|
||||
|
||||
@property
|
||||
def first(self):
|
||||
return self
|
||||
|
||||
def inner_text(self, timeout=None):
|
||||
if self._raises:
|
||||
raise RuntimeError("locator not found")
|
||||
return self._text
|
||||
|
||||
|
||||
# (label, #dispMessage text, should it be treated as a dead end)
|
||||
CASES = [
|
||||
("healthy - notification sent",
|
||||
"eClinicalWorks sent a notification to your email. "
|
||||
"Please confirm it was you.", False),
|
||||
("healthy - 6-digit code prompt",
|
||||
"Enter the 6 digit code sent to your email", False),
|
||||
("timed out",
|
||||
"Your log in request has timed out. Please try again", True),
|
||||
("answered not-me",
|
||||
"You confirmed that you weren't trying to Log in. "
|
||||
"If this was a mistake, please try again.", True),
|
||||
("resends exhausted",
|
||||
"A new notification has been sent to your email, No attempt left.", True),
|
||||
# Case must not matter — eCW has shipped "Log in" and "log in" in the same
|
||||
# sentence, and this is matched on a fragment.
|
||||
("timed out, lowercased",
|
||||
"your log in request has timed out. please try again", True),
|
||||
# Not on the 2FA screen at all: the element is missing and that is normal.
|
||||
("element missing", None, False),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
failures = 0
|
||||
for label, text, want_dead in CASES:
|
||||
page = FakePage(text or "", raises=text is None)
|
||||
why = dead_end.reason(page)
|
||||
got_dead = bool(why)
|
||||
|
||||
raised = False
|
||||
try:
|
||||
dead_end.assert_alive(page, "a test")
|
||||
except LoginTimedOut:
|
||||
raised = True
|
||||
except Exception as exc: # pragma: no cover
|
||||
print(f" FAIL {label}: assert_alive raised {type(exc).__name__}")
|
||||
failures += 1
|
||||
continue
|
||||
|
||||
# reason() and assert_alive() must never disagree — one is the other's
|
||||
# only input.
|
||||
ok = (got_dead == want_dead) and (raised == want_dead)
|
||||
failures += not ok
|
||||
verdict = "dead end" if got_dead else "alive "
|
||||
wanted = "" if ok else \
|
||||
(" (wanted dead end)" if want_dead else " (wanted alive)")
|
||||
# ASCII only. A test that crashes on its own output is worse than no
|
||||
# test, and a cp1252 console cannot encode an arrow — see runlog.start().
|
||||
detail = f" -> {why}" if why else ""
|
||||
print(f" {'ok ' if ok else 'FAIL'} {label:28s} "
|
||||
f"{verdict}{wanted}{detail}")
|
||||
|
||||
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