r""" web/app.py and web/repo.py — the results portal, against a fake repository and a fake database connection (no SQL Server needed). Spec: docs/superpowers/specs/2026-10-19-results-portal-redesign-design.md. python tests\test_web_app.py Claim numbers here are made up. """ import io import sys import json import pathlib import tempfile import contextlib from datetime import date, datetime, timedelta from decimal import Decimal ROOT = pathlib.Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "src")) from fastapi.testclient import TestClient # noqa: E402 from web import app as web_app # noqa: E402 from web.repo import SqlResultsRepo # noqa: E402 from ecw_bot.core import results_db as rdb # noqa: E402 from web import users as portal_users # noqa: E402 sys.path.insert(0, str(ROOT / "tests")) from test_portal_users import FakeUserStore # noqa: E402 CASES = [] def case(fn): CASES.append(fn) return fn DAY = date(2026, 10, 16) RUN_AM = datetime(2026, 10, 16, 10, 30, 0) # 10 claims run RUN_PM = datetime(2026, 10, 16, 14, 15, 0) # 20 claims run, same day RUN_OLD = datetime(2026, 10, 15, 11, 0, 0) def _row(claim, category, s_no, run_at=RUN_PM, **over): row = {c: None for _, c in rdb.COLUMNS} row.update({"id": s_no, "run_at": run_at, "run_mode": "VERIFY", "s_no": s_no, "claim_no": claim, "category": category, "service_date": date(2026, 7, 31), "payer": "Medicaid", "charges": Decimal("1365.00"), "comments": f"comment {claim}", "current_status": "Corrections pending (not saved)", "details_json": json.dumps({"fired": ["dummy_codes"]})}) row.update(over) return row def _day(day, claims, runs, p, f, r): return {"day": day, "claims": claims, "runs": runs, "pass_count": p, "fail_count": f, "review_count": r} class FakeRepo: def __init__(self, days=None, rows=None, fail=False): self._days = days if days is not None else [ _day(DAY, 3, 2, 1, 1, 1), _day(RUN_OLD.date(), 2, 1, 2, 0, 0)] self._rows = rows if rows is not None else [ _row("9000001", "Pass", 1, RUN_AM), _row("9000002", "Fail", 1, details_json=None), _row("9000003", "Review", 2, payer="HCFA Medicare")] self.fail = fail self.calls = [] def _maybe_fail(self): if self.fail: raise self.fail if isinstance(self.fail, Exception) \ else RuntimeError("Login failed for user") def runs(self): self._maybe_fail() return [{"run_at": RUN_PM, "run_mode": "VERIFY", "claims": 2, "pass_count": 0, "fail_count": 1, "review_count": 1}] def results(self, run_at, category=None, claim_no=None): self.calls.append(("results", run_at, category, claim_no)) self._maybe_fail() return [r for r in self._rows if (not category or r["category"] == category)] def days(self, limit=90): self.calls.append(("days",)) self._maybe_fail() return self._days def day_results(self, day): self.calls.append(("day_results", day)) self._maybe_fail() return [dict(r) for r in self._rows if r["run_at"].date() == day] def claim_history(self, claim_no): self.calls.append(("claim_history", claim_no)) self._maybe_fail() return [dict(r) for r in self._rows if claim_no in r["claim_no"]] TESTER_PASSWORD = "tester-password-1" def signed_in(repo, runner, base_url="http://127.0.0.1:8000", role="Admin", **kw): """(client, app): the Portal with one Portal User, signed in — every page needs a Sign-in since 2026-10-22 (EEWA-32). A foreign host gets its 400 before any Sign-in, so there it is not tried.""" store = FakeUserStore() portal_users.Accounts(store).create_user("tester", "Tester", role, TESTER_PASSWORD) # No run events unless a test brings its own: the real Logs\ would # make the Dashboard's last-run banner depend on the PC. kw.setdefault("logs_dir", tempfile.mkdtemp()) app = web_app.create_app(repo, runner=runner, users_store=store, **kw) client = TestClient(app, base_url=base_url) if base_url in web_app.ALLOWED_ORIGINS: r = client.post("/signin", headers={"Origin": base_url}, data={ "username": "tester", "password": TESTER_PASSWORD, "token": app.state.run_token, "next": "/account/password"}) # A page that reads no database: FakeRepo(fail=True) must not # break the Sign-in itself. assert r.status_code == 200, r.status_code assert r.url.path == "/account/password", r.url return client, app def _client(repo=None, base_url="http://127.0.0.1:8000"): repo = repo or FakeRepo() client, _ = signed_in(repo, FakeRunner(), base_url=base_url) return client, repo class FakeRunner: def __init__(self, running=False, stop_fails=None): self.running = running self.stop_fails = stop_fails self.calls = [] def _st(self): return {"running": self.running, "starting": False, "pid": 1, "started_at": "2026-10-14T10:00:00", "ended_at": None, "exit_code": None, "stopped_by_operator": False, "source": "web", "log_path": None, "out_path": None} def start(self, started_by=""): self.calls.append("start") self.started_by = started_by if self.running: raise web_app.AlreadyRunning("A run is already going (started x)") self.running = True return self._st() def stop(self, stopped_by=""): self.calls.append("stop") self.stopped_by = stopped_by if self.stop_fails: raise web_app.StopFailed(self.stop_fails) if not self.running: raise web_app.NotRunning("No run is going") self.running = False return self._st() def status(self): return self._st() def _bot_client(runner=None): runner = runner or FakeRunner() client, app = signed_in(FakeRepo(), runner) return (client, runner, app.state.run_token) _GOOD_ORIGIN = {"Origin": "http://127.0.0.1:8000"} _PAGES = ("/", "/days", f"/day/{DAY.isoformat()}", "/claim?claim_no=9000") def _quiet(fn, *a, **k): with contextlib.redirect_stdout(io.StringIO()): return fn(*a, **k) def _css_rule(html, selector): """What `selector { ... }` in the page's ")] rule = css[css.index("\n " + selector + " {"):] return rule[:rule.index("}")] # ── Dashboard ──────────────────────────────────────────────────────────────── @case def the_dashboard_has_no_claims_table(): """Operator, 2026-10-19: the claims table (three quarters of the page) is not on the home page — the Bot run card, today's counts, Find a claim and one Claim status button are.""" client, repo = _client() html = client.get("/").text assert "30Checked today' \ in html, html assert f'href="/day/{today.isoformat()}?category=Fail">5<' \ in html # No run today: zeros, not yesterday's numbers. html = _client(FakeRepo(days=[_day(today - timedelta(days=1), 9, 1, 9, 0, 0)]))[0].get("/").text assert '0Checked today' \ in html @case def an_old_one_run_link_goes_to_that_runs_day(): client, repo = _client() r = client.get("/", params={"run_at": RUN_OLD.isoformat(), "category": "Fail", "claim_no": " 90-00 "}, follow_redirects=False) assert r.status_code == 303, r.status_code assert r.headers["location"] == \ "/day/2026-10-15?category=Fail&claim_no=9000", r.headers assert repo.calls == [] @case def the_dashboard_rejects_a_bad_filter(): client, _ = _client() assert client.get("/", params={"category": "Bad"}).status_code == 400 assert client.get("/", params={"run_at": "not-a-date"}).status_code == 400 # ── Claim status: date cards ───────────────────────────────────────────────── @case def claim_status_lists_every_day_newest_first(): """Operator, 2026-10-19: a second run hid the first — every day with a run is a card, all its runs together.""" client, _ = _client() html = client.get("/days").text first = html.index('href="/day/2026-10-16"') second = html.index('href="/day/2026-10-15"') assert first < second assert "Fri, 16 Oct 2026" in html and "3 claims checked · 2 runs" in html assert "2 claims checked · 1 run<" in html, "one run, not 'runs'" assert 'data-n="1"' in html, "the colour bar's counts" @case def claim_status_with_no_runs_says_so(): html = _client(FakeRepo(days=[], rows=[]))[0].get("/days").text assert "No results saved yet" in html # ── One day ────────────────────────────────────────────────────────────────── @case def a_day_shows_every_run_of_that_day_together(): client, repo = _client() html = client.get("/day/2026-10-16").text for claim in ("9000001", "9000002", "9000003"): assert f'data-claim="{claim}"' in html, claim assert "10:30 AM" in html and "2:15 PM" in html, "each row's run time" assert "3 claims checked · 2\n runs" in html, html assert repo.calls == [("day_results", DAY)], repo.calls # The short table only — no 24-column sideways table. for header in ("Claim #", "Time", "Payer", "Result", "Comments"): assert f"{header}" in html, header assert "SERVICE DATE" not in html[html.index("")] @case def a_days_tiles_filter_but_still_count_the_whole_day(): client, _ = _client() html = client.get("/day/2026-10-16", params={"category": "Fail"}).text assert 'data-claim="9000002"' in html assert 'data-claim="9000001"' not in html assert 'class="tile Fail on"' in html assert '3All' in html, \ "the All tile counts the whole day" html = client.get("/day/2026-10-16", params={"claim_no": "0003"}).text assert 'data-claim="9000003"' in html and 'data-claim="9000002"' not in html assert "?category=Fail&claim_no=0003" in html, "tiles keep the search" @case def a_day_row_carries_every_field_for_its_side_panel(): html = _client()[0].get("/day/2026-10-16").text row = html[html.index('data-claim="9000003"'):] row = row[:row.index("")] fields = json.loads(row[row.index("data-fields='") + 13: row.index("' data-details")] .replace("'", "'")) assert ["CLAIMS#", "9000003"] in fields, fields assert ["SERVICE DATE", "07/31/2026"] in fields assert ["Charges", "1,365.00"] in fields or any( v == "1,365.00" for _, v in fields), fields assert ["Mode", "VERIFY"] in fields assert "dummy_codes" in row, "the conditions JSON is on the row" @case def a_day_opens_a_claim_from_the_link(): html = _client()[0].get("/day/2026-10-16", params={"claim": "9000002"}).text assert 'const want = "9000002";' in html assert "showClaim(allRows[i]);" in html @case def a_day_with_nothing_says_so_and_a_bad_day_is_400(): client, _ = _client() assert "No claims were checked on this day" in \ client.get("/day/2026-01-01").text html = client.get("/day/2026-10-16", params={"category": "Pass", "claim_no": "0002"}).text assert "No claims match this filter" in html for bad in ("/day/16-10-2026", "/day/2026-13-01", "/day/x"): assert client.get(bad).status_code == 400, bad assert client.get("/day/2026-10-16", params={"category": "Bad"}).status_code == 400 @case def claim_text_reaches_the_page_escaped(): evil = "'" rows = [_row("9000009", "Fail", 1, comments=evil, payer=evil)] html = _client(FakeRepo(rows=rows))[0].get("/day/2026-10-16").text assert "