Files
ecw-charge-validation/tests/test_dead_end.py
T
ManikandanandClaude Opus 5.5 ba15a574ab 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>
2026-10-23 18:38:17 +05:30

106 lines
3.6 KiB
Python

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