Files
fxhnt/tests/integration/test_web_overview_nav.py
2026-06-24 23:05:35 +02:00

116 lines
4.9 KiB
Python

"""Cockpit reorg: grouped nav (Live/Backtest/Research) resolves, the at-a-glance Overview home renders
book-status + edges + a chart, and the rendered pages stay leak-clean (no absolute app links / external
CDNs) so the nginx tailnet proxy never exposes the internal address. All seeded in-memory — no DB, no net."""
from __future__ import annotations
import datetime as dt
import re
from fastapi.testclient import TestClient
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.adapters.web.app import create_app
from fxhnt.application.forward_models import BacktestSummary
from fxhnt.application.forward_models import ForwardNavRow as Row
from fxhnt.application.forward_models import ForwardSummary
# Every nav-group link in base.html. Each must resolve 200 (forward track empty is fine — routes degrade).
_NAV_LINKS = ["/", "/paper", "/paper/replay", "/paper/sim", "/funnel", "/factory"]
def _client(*, seed_headline: bool = True) -> TestClient:
repo = ForwardNavRepo("sqlite://")
repo.migrate()
if seed_headline:
at = dt.datetime(2026, 6, 24, 12, 0)
# Multi-day forward history for the bybit_4edge headline book so the NAV curve + countdown render.
rows = [Row("bybit_4edge", f"2026-06-{5 + i:02d}", 0.004, 1.0 + 0.004 * (i + 1)) for i in range(8)]
repo.upsert_rows(rows, at)
repo.upsert_summary(
ForwardSummary("bybit_4edge", "2026-06-12", 8, 1.032, 0.032, 1.4, -0.02),
"WAIT", "8/60 forward days", at)
repo.upsert_backtest_summary(BacktestSummary(
"bybit_4edge", "2026-05-30", cagr=0.30, ann_vol=0.20, sharpe=1.5, max_drawdown=-0.16,
passed=True, dsr=0.7, is_sharpe=1.6, oos_sharpe=1.3, pvalue=0.01), at)
return TestClient(create_app(repo))
# ---- grouped nav --------------------------------------------------------------------------------------
def test_nav_groups_present() -> None:
r = _client().get("/")
assert r.status_code == 200
for label in ("Live", "Backtest", "Research", "Overview", "Paper", "Replay", "Run sim", "Funnel", "Factory"):
assert label in r.text, f"nav label {label!r} missing"
def test_every_nav_link_resolves() -> None:
c = _client()
for href in _NAV_LINKS:
assert c.get(href).status_code == 200, f"nav link {href} did not resolve"
def test_active_section_highlighted() -> None:
c = _client()
# Backtest section is active on /paper/sim; Live section active on the home page.
assert 'class="group active"' in c.get("/paper/sim").text
assert 'class="group active"' in c.get("/").text
# ---- at-a-glance overview home ------------------------------------------------------------------------
def test_overview_shows_book_status_and_countdown() -> None:
r = _client().get("/")
assert r.status_code == 200
assert "Fund status" in r.text
assert "Forward Sharpe" in r.text and "OOS Sharpe" in r.text and "Max drawdown" in r.text
# gate countdown computed from gate_spec.min_days (60) - days (8) = 52, not hardcoded.
assert "52" in r.text and "days left" in r.text
assert "Bybit 4-edge book" in r.text # headline display name
def test_overview_shows_edges_and_links() -> None:
r = _client().get("/")
assert r.status_code == 200
assert "Edges in the book" in r.text
assert 'class="fleet"' in r.text # the per-edge table is preserved
assert "/strategy/" in r.text # each edge links to its detail
def test_overview_renders_forward_nav_chart() -> None:
r = _client().get("/")
assert r.status_code == 200
assert "Forward NAV" in r.text
assert "<svg" in r.text and "<polyline" in r.text # inline-SVG chart rendered, no JS lib
def test_overview_honest_when_no_forward_history() -> None:
# No headline seeded: home must still render (never 500) and be honest about the missing track.
r = _client(seed_headline=False).get("/")
assert r.status_code == 200
assert "Fund status" in r.text
# ---- leak-clean guard (protects the nginx tailnet proxy) ----------------------------------------------
# An absolute http(s) URL anywhere in the rendered HTML. The only legitimate absolute URL is the SVG
# xmlns namespace (www.w3.org), which is not a fetched resource — allow exactly that, reject everything
# else (CDNs, absolute app/host links would leak the internal tailnet address).
_ABS_URL = re.compile(r"https?://(?!www\.w3\.org/)[^\s\"'<>]+")
def _assert_leak_clean(html: str) -> None:
hits = _ABS_URL.findall(html)
assert not hits, f"leak-clean violation: absolute URLs in rendered HTML: {hits}"
def test_home_is_leak_clean() -> None:
_assert_leak_clean(_client().get("/").text)
def test_base_chrome_is_leak_clean() -> None:
# base.html chrome (nav, header, vendored htmx/img) must emit only relative URLs across every route.
c = _client()
for href in _NAV_LINKS:
_assert_leak_clean(c.get(href).text)