from pathlib import Path ROOT = Path(__file__).resolve().parents[1] RUNTIME = (ROOT / "ui_builder" / "static" / "runtime.html").read_text(encoding="utf-8") EDITOR = (ROOT / "ui_builder" / "static" / "index.html").read_text(encoding="utf-8") INTEGRATION = (ROOT / "ui_builder" / "integration.py").read_text(encoding="utf-8") APP_JS = (ROOT / "ui_builder" / "static" / "app.js").read_text(encoding="utf-8") TOURNAMENT_JS = (ROOT / "hockey_data" / "static" / "tournament-menu.js").read_text(encoding="utf-8") APP_PY = (ROOT / "app.py").read_text(encoding="utf-8") def test_runtime_has_no_builder_dom(): assert 'id="editor"' not in RUNTIME assert 'id="componentLibrary"' not in RUNTIME assert 'id="canvasStage"' not in RUNTIME assert 'id="inspector"' not in RUNTIME assert 'class="topbar editor-only"' not in RUNTIME assert 'id="runtimeView"' in RUNTIME assert 'runtime-view runtime-only hidden' not in RUNTIME def test_editor_keeps_full_builder(): assert 'id="editor"' in EDITOR assert 'id="componentLibrary"' in EDITOR assert 'id="canvasStage"' in EDITOR assert 'id="inspector"' in EDITOR def test_integration_uses_separate_runtime_template(): assert 'runtime_template = (assets_dir / "runtime.html")' in INTEGRATION assert 'template = editor_template if mode == "editor" else runtime_template' in INTEGRATION def test_editor_button_is_admin_only_and_hotkey_respects_it(): assert 'id="runtimeEditorBtn" class="icon-btn hidden"' in RUNTIME assert 'editorButton?.classList.toggle("hidden", !state.isAdmin);' in TOURNAMENT_JS assert 'document.body.dataset.hockeyAdmin = state.isAdmin ? "1" : "0";' in TOURNAMENT_JS assert 'el.runtimeEditorBtn?.classList.contains("hidden")' in APP_JS def test_editor_server_paths_are_admin_protected(): """Execute the actual middleware with local identities; never import the live app.""" import ast import asyncio from types import SimpleNamespace from fastapi import HTTPException from fastapi.responses import JSONResponse, RedirectResponse tree = ast.parse(APP_PY) middleware = next(node for node in tree.body if isinstance(node, ast.AsyncFunctionDef) and node.name == "hockey_runtime_auth") middleware.decorator_list = [] current = SimpleNamespace(user=SimpleNamespace(is_admin=False)) async def optional_user(request): return current.user namespace = dict(hockey_auth=SimpleNamespace(optional_user=optional_user), Request=object, HTTPException=HTTPException, JSONResponse=JSONResponse, RedirectResponse=RedirectResponse, apply_web_cache_policy=lambda request, response: response) exec(compile(ast.fix_missing_locations(ast.Module(body=[middleware], type_ignores=[])), "", "exec"), namespace) async def scenario(): async def following(request): return JSONResponse({"allowed": True}) async def check(path, method, expected): request = SimpleNamespace(url=SimpleNamespace(path=path), method=method, state=SimpleNamespace()) response = await namespace["hockey_runtime_auth"](request, following) assert response.status_code == expected, (path, method, response.body) return response assert (await check("/editor", "GET", 303)).headers["location"] == "/" await check("/api/ui-builder/editor/config", "GET", 403) await check("/api/ui-builder/auth/status", "GET", 403) await check("/khl-site/api/players", "POST", 403) await check("/khl-site/", "GET", 200) await check("/", "GET", 200) current.user = SimpleNamespace(is_admin=True) await check("/editor", "GET", 200) await check("/api/ui-builder/editor/config", "POST", 200) current.user = None assert (await check("/editor", "GET", 303)).headers["location"].startswith("/login") await check("/api/ui-builder/auth/status", "GET", 401) asyncio.run(scenario())