#!/usr/bin/env python3
"""web panel — static and contract tests (stdlib unittest).
No port is bound, no browser is opened and no server process is started: the
whole HTTP surface is covered through ``web.panel.route()``, which is a pure
function, and every rendering claim is checked on the rendered bytes.
What these tests defend (the things that could silently regress):
* the Turkish status vocabulary still matches docs/DESIGN.md exactly, so the
device console and the central panel cannot drift apart;
* every user-visible string is non-empty, Turkish and free of placeholders;
* no external URL, no webfont, no hex colour and no new colour token anywhere
in the templates or stylesheets: colour comes from tokens.css only;
* the panel imports nothing outside the standard library and can therefore be
installed on the air-gapped fleet it manages;
* only GET/HEAD are served, static assets are allow-listed, and a full sweep
over every endpoint leaves the filesystem untouched;
* the first viewport is ordered and populated as docs/DESIGN.md requires.
Run:
python3 tests/test_web_static.py -v
"""
from __future__ import annotations
import ast
import contextlib
import io
import json
import re
import sys
import tempfile
import unittest
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parents[1]
WEB_DIR = REPO_ROOT / "web"
TEMPLATES_DIR = WEB_DIR / "templates"
STATIC_DIR = WEB_DIR / "static"
TOKENS_CSS = STATIC_DIR / "tokens.css"
DESIGN_DOC = REPO_ROOT / "docs" / "DESIGN.md"
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
import web # noqa: E402 (path set above)
from web import fleet_data, panel, render, server # noqa: E402
# The panel must be the one in this checkout, not some unrelated "web" package.
assert Path(web.__file__).resolve().parent == WEB_DIR, f"yanlis paket: {web.__file__}"
NOW = "2026-09-16T22:00:00+03:00"
STATE_WORDS = ("KURULDU", "KAYITLI", "HAZIR", "HATA", "YOK")
# Everything an air-gapped, framework-free surface must never contain.
FORBIDDEN_ASSET_MARKERS = (
"http://",
"https://",
"//cdn",
"cdn.jsdelivr",
"cdnjs",
"unpkg",
"fonts.googleapis",
"fonts.gstatic",
"@import",
"@font-face",
"src=",
"xlink:href=",
)
FORBIDDEN_IMPORT_ROOTS = ("fastapi", "flask", "jinja2", "uvicorn", "django", "werkzeug", "starlette", "requests")
FORBIDDEN_IMPORTS = ("urllib.request", "http.client", "socket", "subprocess", "shutil", "ftplib")
FORBIDDEN_SOURCE_MARKERS = ("os.system(", "subprocess.", "urllib.request", "requests.", "eval(", "exec(")
HEX_COLOUR = re.compile(r"#[0-9a-fA-F]{3,8}\b")
CUSTOM_PROPERTY = re.compile(r"^\s*--[A-Za-z0-9-]+\s*:", re.M)
# A custom-property declaration, e.g. `--state-ready: var(--ok);`
CUSTOM_PROPERTY_DECL = re.compile(r"^\s*--([A-Za-z0-9-]+)\s*:\s*([^;]+);", re.M)
# A single `var(--name)` reference inside a value.
VAR_REFERENCE = re.compile(r"var\(\s*--([A-Za-z0-9-]+)\s*\)")
DEFINED_TOKEN = re.compile(r"^\s*(--[a-z0-9-]+)\s*:", re.M)
USED_TOKEN = re.compile(r"var\((--[a-z0-9-]+)\)")
TEXT_TAGS = ("td", "th", "span", "h1", "h2", "h3", "p", "li", "option", "dd", "dt", "button", "a", "caption", "label")
ELEMENT = re.compile(r"<(%s)\b[^>]*>(.*?)\1>" % "|".join(TEXT_TAGS), re.S)
ROW_BADGE = re.compile(r']*>([^<]*)')
# --------------------------------------------------------------------------
# helpers
# --------------------------------------------------------------------------
def token_values(text: str) -> dict[str, str]:
"""Every custom-property declaration in a stylesheet, name -> raw value."""
return {
name: value.strip()
for name, value in CUSTOM_PROPERTY_DECL.findall(text)
}
def resolved_colour(name: str, values: dict[str, str], limit: int = 12) -> str | None:
"""Follow ``var(--x)`` aliases to the colour a token actually paints.
Comparing raw declarations is not enough to prove two tokens differ: a
mutation writing ``--state-provisioned: var(--accent)`` leaves the strings
unequal while painting exactly the same blue. Only the resolved value is
evidence.
"""
seen: set[str] = set()
while limit > 0:
value = values.get(name)
if value is None:
return None
reference = VAR_REFERENCE.fullmatch(value)
if reference is None:
return value
name = reference.group(1)
if name in seen:
return value # a cycle: report what was declared rather than loop
seen.add(name)
limit -= 1
return values.get(name)
def design_vocabulary() -> list[tuple[str, str]]:
"""Read the status vocabulary table out of docs/DESIGN.md."""
text = DESIGN_DOC.read_text(encoding="utf-8")
row = re.compile(r"^\|\s*`([^`]+)`\s*\|\s*`(--state-[a-z-]+)`\s*\|", re.M)
return row.findall(text)
def text_files() -> list[Path]:
"""Templates and stylesheets: everything the browser receives from disk."""
return sorted(TEMPLATES_DIR.glob("*.html")) + sorted(STATIC_DIR.glob("*.css"))
def empty_elements(markup: str) -> list[str]:
"""Visible elements that carry no text at all (an empty label is a bug)."""
problems: list[str] = []
for match in ELEMENT.finditer(markup):
inner = re.sub(r"<[^>]+>", "", match.group(2))
if not inner.strip():
problems.append(match.group(0)[:140])
return problems
def tree_fingerprint(root: Path) -> tuple:
"""Sizes and mtimes of every source file, ignoring interpreter caches."""
entries = []
for path in sorted(root.rglob("*")):
if "__pycache__" in path.parts or not path.is_file():
continue
stat = path.stat()
entries.append((str(path.relative_to(root)), stat.st_size, stat.st_mtime_ns))
return tuple(entries)
class PanelCase(unittest.TestCase):
"""Shared fixture: the labelled demo snapshot and both privilege levels."""
@classmethod
def setUpClass(cls) -> None:
cls.snapshot = fleet_data.demo_snapshot(NOW)
cls.read_only = render.privilege_from_level("read-only")
cls.full = render.privilege_from_level("full")
cls.info = render.ServerInfo(
host="127.0.0.1",
port=8787,
privilege=cls.read_only,
python_version="3.13.0",
started_at=NOW,
)
cls.context = panel.PanelContext(
snapshot=cls.snapshot, privilege=cls.read_only, server_info=cls.info
)
full_info = render.ServerInfo(
host="127.0.0.1",
port=8787,
privilege=cls.full,
python_version="3.13.0",
started_at=NOW,
)
cls.full_context = panel.PanelContext(
snapshot=cls.snapshot, privilege=cls.full, server_info=full_info
)
# -- request helpers ---------------------------------------------------
def get(self, target: str, method: str = "GET", context: panel.PanelContext | None = None):
return panel.route(context or self.context, method, target)
def page(self, target: str = "/", method: str = "GET", context: panel.PanelContext | None = None) -> str:
response = self.get(target, method, context)
self.assertEqual(response.status, 200, f"{target}: {response.status} (beklenen 200)")
return response.body.decode("utf-8")
def list_region(self, page: str) -> str:
"""The device table only, so about/detail badges never leak into a scan."""
start = page.index('data-region="list"')
end = page.index('data-region="detail"')
return page[start:end]
def row_words(self, page: str) -> list[str]:
return [word for _, word in ROW_BADGE.findall(self.list_region(page))]
def snapshot_from(self, document: object, *, now: str = NOW, filename: str = "fleet.json"):
"""Write a temporary inventory document and load it through the loader."""
directory = tempfile.TemporaryDirectory(prefix="bf-panel-test-")
self.addCleanup(directory.cleanup)
path = Path(directory.name) / filename
if isinstance(document, str):
path.write_text(document, encoding="utf-8")
else:
path.write_text(json.dumps(document, ensure_ascii=False), encoding="utf-8")
return fleet_data.load_fleet(path, env={}, now=now)
# --------------------------------------------------------------------------
# vocabulary and design contract
# --------------------------------------------------------------------------
class TestVocabularyMatchesDesign(PanelCase):
def test_vocabulary_matches_docs_design_exactly(self) -> None:
"""One word per state, shared with the device console; no fifth word."""
self.assertEqual(
design_vocabulary(),
[(spec.word, spec.token) for spec in fleet_data.STATE_VOCABULARY],
)
def test_design_document_still_has_the_five_words(self) -> None:
words = [word for word, _ in design_vocabulary()]
self.assertEqual(words, list(STATE_WORDS))
def test_every_word_is_upper_case_turkish_and_non_empty(self) -> None:
for spec in fleet_data.STATE_VOCABULARY:
with self.subTest(state=spec.state):
self.assertTrue(spec.word.strip(), spec.state)
self.assertEqual(spec.word, spec.word.upper())
self.assertTrue(spec.meaning.strip(), spec.word)
self.assertNotIn("None", spec.meaning)
def test_tokens_exist_in_tokens_css(self) -> None:
css = TOKENS_CSS.read_text(encoding="utf-8")
for spec in fleet_data.STATE_VOCABULARY:
with self.subTest(state=spec.state):
self.assertIn(f"{spec.token}:", css)
self.assertIn(f"{spec.soft_token}:", css)
def test_non_ready_states_sort_before_ready(self) -> None:
self.assertEqual(fleet_data.STATE_ORDER[-1], "READY")
self.assertEqual(set(fleet_data.STATE_ORDER), set(fleet_data.STATE_BY_NAME))
def test_demo_data_exercises_every_state_word(self) -> None:
counts = self.snapshot.counts
self.assertEqual(list(counts), list(STATE_WORDS))
for word, count in counts.items():
with self.subTest(word=word):
self.assertGreaterEqual(count, 1, f"{word} demo veride yok; sözlük test edilemez")
def test_demo_data_exercises_every_wave(self) -> None:
counts = fleet_data.wave_counts(self.snapshot.devices)
self.assertEqual(list(counts), [wave.code for wave in fleet_data.WAVES])
for code, count in counts.items():
with self.subTest(wave=code):
self.assertGreaterEqual(count, 1)
def test_waves_match_the_rollout_document(self) -> None:
"""docs/18: LAB(2) -> P1(5) -> P2(20) -> W1(50) -> W2(100) -> PROD."""
self.assertEqual(
[(wave.code, wave.target) for wave in fleet_data.WAVES],
[("LAB", 2), ("P1", 5), ("P2", 20), ("W1", 50), ("W2", 100), ("PROD", 523)],
)
def test_four_documented_channels(self) -> None:
self.assertEqual(
[channel.label for channel in fleet_data.CHANNELS],
["SSH-over-WG", "xRDP", "RustDesk", "MeshCentral"],
)
self.assertEqual(
[channel.key for channel in fleet_data.CHANNELS],
["ssh_over_wg", "xrdp", "rustdesk", "meshcentral"],
)
def test_channel_status_words_are_turkish_and_non_empty(self) -> None:
for status, word in fleet_data.CHANNEL_STATUS_WORDS.items():
with self.subTest(status=status):
self.assertTrue(word.strip())
self.assertNotIn("None", word)
self.assertEqual(set(fleet_data.CHANNEL_STATUS_WORDS), set(fleet_data.CHANNEL_STATUSES))
# --------------------------------------------------------------------------
# no external assets, no invented colour
# --------------------------------------------------------------------------
class TestAssetsAndColour(PanelCase):
def test_no_external_url_or_remote_asset_in_any_template_or_stylesheet(self) -> None:
for path in text_files():
text = path.read_text(encoding="utf-8").lower()
for marker in FORBIDDEN_ASSET_MARKERS:
with self.subTest(file=path.name, marker=marker):
self.assertNotIn(marker, text)
def test_no_javascript_ships_with_the_panel(self) -> None:
for path in text_files():
text = path.read_text(encoding="utf-8").lower()
with self.subTest(file=path.name):
self.assertNotIn("