import unittest from pathlib import Path from unittest.mock import patch from urllib.parse import parse_qs, urlsplit from fastapi import FastAPI from fastapi.testclient import TestClient from app.frontend.router import router from app.identity import ( get_current_user, router as identity_router, safe_next_path, install_identity_middleware, protected_page, required_page_permission, ) ROUTES = ( "/dashboard", "/monitoring/map", "/operations/repairs", "/assets/miners", ) STATIC_ROOT = Path(__file__).parents[1] / "app" / "frontend" / "static" class FrontendShellTests(unittest.TestCase): def setUp(self): self.app = FastAPI() self.app.include_router(router) self.client = TestClient(self.app) def tearDown(self): self.client.close() def authenticate(self, permissions=None, role="Read Only"): self.app.dependency_overrides[get_current_user] = lambda: { "id": "00000000-0000-0000-0000-000000000321", "username": "operator", "display_name": "Floor Operator", "role": role, "permissions": list( permissions or ( "dashboard.view", "locations.view", "repairs.view", "miners.view", ) ), } def test_routes_require_authentication(self): for path in ROUTES: with self.subTest(path=path): self.assertEqual(self.client.get(path).status_code, 401) def test_all_direct_routes_render_authenticated_shell(self): self.authenticate() for path in ROUTES: with self.subTest(path=path): response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertIn("MinerOps", response.text) self.assertIn( "Operational Intelligence for Mining", response.text, ) self.assertIn("Floor Operator", response.text) self.assertIn( 'aria-label="Primary navigation"', response.text, ) def test_navigation_connects_all_internal_alpha_views(self): self.authenticate() response = self.client.get("/dashboard") for path in ROUTES: self.assertIn(f'href="{path}"', response.text) self.assertIn('aria-current="page"', response.text) def test_each_route_requires_its_specific_permission(self): cases = ( ("/dashboard", "dashboard.view"), ("/monitoring/map", "locations.view"), ("/operations/repairs", "repairs.view"), ("/assets/miners", "miners.view"), ) for path, permission in cases: with self.subTest(path=path): self.authenticate(permissions=("unrelated.permission",)) self.assertEqual(self.client.get(path).status_code, 403) self.authenticate(permissions=(permission,)) self.assertEqual(self.client.get(path).status_code, 200) def test_main_authentication_gate_redirects_to_existing_login(self): app = FastAPI() app.include_router(router) install_identity_middleware(app) with TestClient(app) as client, patch( "app.identity.request_user_or_none", return_value=None, ): for path in ROUTES: with self.subTest(path=path): response = client.get(path, follow_redirects=False) self.assertEqual(response.status_code, 303) self.assertTrue( response.headers["location"].startswith( "/login?next=" ) ) def test_page_permission_mapping_covers_new_routes(self): expected = { "/dashboard": "dashboard.view", "/monitoring/map": "locations.view", "/operations/repairs": "repairs.view", "/assets/miners": "miners.view", } for path, permission in expected.items(): with self.subTest(path=path): self.assertTrue(protected_page(path)) self.assertEqual( required_page_permission(path), permission, ) def test_dashboard_does_not_fabricate_operational_values(self): self.authenticate() response = self.client.get("/dashboard") self.assertIn( "Live health summary not yet connected", response.text, ) self.assertIn("No events are fabricated", response.text) self.assertNotIn("100 TH/s", response.text) def test_map_is_a_structured_operational_view(self): self.authenticate() response = self.client.get("/monitoring/map") for label in ( "Site", "Room", "Operational Status", "Miner details", "Find miner", ): self.assertIn(label, response.text) for value in ( 'id="mapSite"', 'id="mapRoom"', 'id="mapLayer"', 'id="mapViewport"', 'id="mapInspector"', '/assets/minerops-mine-map.js', ): self.assertIn(value, response.text) self.assertNotIn("Live layout and telemetry data are not", response.text) def test_data_views_include_all_request_states(self): self.authenticate() for path in ("/operations/repairs", "/assets/miners"): with self.subTest(path=path): response_text = self.client.get(path).text for state in ( "loading", "empty", "error", "denied", "ready", ): self.assertIn( f'data-view-state="{state}"', response_text, ) def test_frontend_assets_are_responsive_and_accessible(self): css = (STATIC_ROOT / "app.css").read_text(encoding="utf-8") javascript = (STATIC_ROOT / "app.js").read_text( encoding="utf-8" ) self.assertIn("@media (max-width: 560px)", css) self.assertIn(":focus-visible", css) self.assertIn(".skip-link", css) self.assertIn('response.status === 401', javascript) self.assertIn('response.status === 403', javascript) self.assertIn('credentials: "same-origin"', javascript) def test_static_assets_are_served_without_an_api_route(self): css_response = self.client.get("/assets/minerops-app.css") script_response = self.client.get("/assets/minerops-app.js") self.assertEqual(css_response.status_code, 200) self.assertEqual( css_response.headers["content-type"], "text/css; charset=utf-8", ) self.assertEqual(script_response.status_code, 200) paths = {route.path for route in router.routes} self.assertFalse(any(path.startswith("/api/") for path in paths)) def test_live_app_prefers_new_shell_and_preserves_legacy_pages(self): from app.main import app first_handlers = {} for route in app.routes: if route.path not in first_handlers: first_handlers[route.path] = route.endpoint for path in ROUTES: with self.subTest(path=path): self.assertEqual( first_handlers[path].__module__, "app.frontend.router", ) for path in ( "/repairs", "/locations", "/miners/{miner_id}/detail", ): with self.subTest(legacy_path=path): self.assertIn(path, first_handlers) def test_safe_next_path_accepts_only_protected_local_pages(self): valid = ( "/dashboard", "/assets/miners?query=S19%20Pro", "/miners/00000000-0000-0000-0000-000000000123/detail", ) for value in valid: with self.subTest(valid=value): self.assertEqual(safe_next_path(value), value) invalid = ( None, "", "https://example.invalid/dashboard", "//example.invalid/dashboard", r"\\example.invalid\dashboard", "/../login", "/assets/%2f%2fexample.invalid", "/dashboard#fragment", "/login", "/api/inventory/miners", " /dashboard", "/dashboard\n", ) for value in invalid: with self.subTest(invalid=value): self.assertEqual(safe_next_path(value), "/dashboard") def test_login_redirect_preserves_direct_link_query(self): app = FastAPI() app.include_router(identity_router) app.include_router(router) install_identity_middleware(app) with TestClient(app) as client, patch( "app.identity.request_user_or_none", return_value=None, ): response = client.get( "/assets/miners?query=S19%20Pro", follow_redirects=False, ) self.assertEqual(response.status_code, 303) location = urlsplit(response.headers["location"]) self.assertEqual(location.path, "/login") self.assertEqual( parse_qs(location.query)["next"], ["/assets/miners?query=S19%20Pro"], ) def test_authenticated_login_redirect_rejects_unsafe_next(self): app = FastAPI() app.include_router(identity_router) install_identity_middleware(app) with TestClient(app) as client, patch( "app.identity.request_user_or_none", return_value={"id": "user"}, ): for value in ( "https://example.invalid", "//example.invalid", r"\\example.invalid\dashboard", "/../login", ): with self.subTest(value=value): response = client.get( "/login", params={"next": value}, follow_redirects=False, ) self.assertEqual(response.status_code, 303) self.assertEqual( response.headers["location"], "/dashboard", ) def test_login_template_escapes_safe_query_destination(self): app = FastAPI() app.include_router(identity_router) hostile = '/assets/miners?query=">' with TestClient(app) as client: response = client.get("/login", params={"next": hostile}) self.assertEqual(response.status_code, 200) self.assertNotIn(hostile, response.text) self.assertIn(""><script>", response.text) self.assertNotIn("new URLSearchParams(location.search)", response.text) def test_user_values_are_html_escaped(self): hostile = '' self.app.dependency_overrides[get_current_user] = lambda: { "id": "00000000-0000-0000-0000-000000000321", "username": "operator", "display_name": hostile, "role": "Read Only", "permissions": ["dashboard.view", "miners.view"], } response = self.client.get("/dashboard") self.assertEqual(response.status_code, 200) self.assertNotIn(hostile, response.text) self.assertIn("<img src=x onerror=alert(1)>", response.text) def test_navigation_and_search_are_permission_aware(self): self.authenticate(permissions=("dashboard.view",)) response = self.client.get("/dashboard") self.assertEqual(response.status_code, 200) self.assertIn('href="/dashboard"', response.text) for path in ROUTES[1:]: self.assertNotIn(f'href="{path}"', response.text) self.assertNotIn('id="globalSearchForm"', response.text) self.assertIn('data-can-view-miners="false"', response.text) def test_assets_disable_caching_and_missing_assets_are_404(self): for path in ( "/assets/minerops-app.css", "/assets/minerops-app.js", "/assets/minerops-mine-map.js", ): with self.subTest(path=path): response = self.client.get(path) self.assertEqual(response.status_code, 200) self.assertEqual(response.headers["cache-control"], "no-store") self.assertEqual( self.client.get("/assets/missing-minerops-file.js").status_code, 404, ) def test_trailing_slash_redirects_are_consistent(self): self.authenticate() for path in ROUTES: with self.subTest(path=path): response = self.client.get( path + "/", follow_redirects=False, ) self.assertEqual(response.status_code, 307) self.assertEqual( urlsplit(response.headers["location"]).path, path, ) def test_shell_markup_has_accessibility_landmarks(self): self.authenticate() for path in ROUTES: with self.subTest(path=path): text = self.client.get(path).text self.assertEqual(text.count("

"), 1) self.assertIn('class="skip-link"', text) self.assertIn('