90 lines
4.0 KiB
Python
90 lines
4.0 KiB
Python
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=[])),
|
|
"<production hockey_runtime_auth>", "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())
|