Chromium kiosk without a desktop for the Raspberry Pi 3, a Flask web interface on port 8080 (kiosk, system and network management), a restricted root helper, network changes with automatic rollback, a healthcheck timer, install/update/uninstall scripts and a pi-gen image build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
143 lines
4.8 KiB
Python
143 lines
4.8 KiB
Python
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from voxip_kiosk.paths import Paths
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
@pytest.fixture
|
|
def paths(tmp_path) -> Paths:
|
|
p = Paths(
|
|
config_dir=tmp_path / "etc",
|
|
state_dir=tmp_path / "state",
|
|
network_state_dir=tmp_path / "netstate",
|
|
agent_run_dir=tmp_path / "run-agent",
|
|
browser_run_dir=tmp_path / "run-browser",
|
|
health_run_dir=tmp_path / "run-health",
|
|
share_dir=tmp_path / "share",
|
|
log_dir=tmp_path / "log",
|
|
helper=tmp_path / "helper",
|
|
nm_connections_dir=tmp_path / "nm",
|
|
)
|
|
for d in (p.config_dir, p.state_dir, p.agent_run_dir, p.browser_run_dir, p.health_run_dir, p.share_dir / "www",
|
|
p.log_dir, p.nm_connections_dir):
|
|
d.mkdir(parents=True)
|
|
(p.share_dir / "VERSION").write_text("9.9.9-test\n")
|
|
default = json.loads((ROOT / "config" / "config.default.json").read_text())
|
|
p.config_file.write_text(json.dumps(default))
|
|
return p
|
|
|
|
|
|
class FakeHelper:
|
|
def __init__(self):
|
|
self.calls = []
|
|
self.fail = None
|
|
self.responses = {}
|
|
|
|
def call(self, verb, *args, payload=None, timeout=60):
|
|
from voxip_kiosk.helper_client import HelperError
|
|
|
|
self.calls.append((verb, args, payload))
|
|
if self.fail:
|
|
raise HelperError(self.fail)
|
|
return {"ok": True, "message": f"{verb} ok", **self.responses.get(verb, {})}
|
|
|
|
|
|
class FakeCDP:
|
|
def __init__(self):
|
|
self.calls = []
|
|
self.fail = None
|
|
|
|
def _do(self, name, *args):
|
|
from voxip_kiosk.cdp import CDPError
|
|
|
|
self.calls.append((name, args))
|
|
if self.fail:
|
|
raise CDPError(self.fail)
|
|
|
|
def reload(self, ignore_cache=False):
|
|
self._do("reload")
|
|
|
|
def screenshot(self, quality=70):
|
|
self._do("screenshot")
|
|
return b"\xff\xd8fakejpeg"
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_helper():
|
|
return FakeHelper()
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_cdp():
|
|
return FakeCDP()
|
|
|
|
|
|
@pytest.fixture
|
|
def quiet_system(monkeypatch):
|
|
"""Keep the web tests independent of the machine they run on."""
|
|
from voxip_kiosk import netinfo, services, sysinfo
|
|
|
|
monkeypatch.setattr(services, "summary", lambda: {
|
|
"browser": {"unit": services.BROWSER_UNIT, "status": "ok", "label": "Actief", "restarts": "2", "since": ""},
|
|
"x": {"unit": services.X_UNIT, "status": "ok", "label": "Actief", "restarts": "0", "since": ""},
|
|
"agent": {"unit": services.AGENT_UNIT, "status": "ok", "label": "Actief", "restarts": "0", "since": ""},
|
|
"manager": {"unit": services.MANAGER_UNIT, "status": "ok", "label": "Actief", "restarts": "0", "since": ""},
|
|
})
|
|
monkeypatch.setattr(services, "recent_errors", lambda lines=20: [])
|
|
monkeypatch.setattr(services, "journal", lambda source, lines=100, priority=None: f"log:{source}:{lines}:{priority}")
|
|
monkeypatch.setattr(sysinfo, "ip_addresses", lambda: {"eth0": ["192.168.10.20/24"]})
|
|
monkeypatch.setattr(sysinfo, "throttled", lambda: {"raw": "0x50000", "now": False, "ever": True, "flags": ["Onderspanning opgetreden"]})
|
|
monkeypatch.setattr(netinfo, "overview", lambda: {
|
|
"available": True, "wifi_country": "NL",
|
|
"devices": {
|
|
iface: {"interface": iface, "kind": kind, "present": True, "state": "connected", "state_label": "Verbonden",
|
|
"connected": True, "connection": f"voxip-{iface}", "addresses": ["192.168.10.20/24"], "gateway": "192.168.10.1",
|
|
"dns": ["192.168.10.1"], "hwaddr": "B8:27:EB:00:00:01",
|
|
"profile": {"exists": True, "method": "dhcp", "address": "", "gateway": "", "dns": "", "ssid": "Kantoor", "hidden": False}}
|
|
for iface, kind in (("eth0", "ethernet"), ("wlan0", "wifi"))
|
|
},
|
|
})
|
|
|
|
|
|
@pytest.fixture
|
|
def app(paths, fake_helper, fake_cdp, quiet_system):
|
|
from voxip_kiosk.web import create_app
|
|
|
|
application = create_app(paths, helper=fake_helper, cdp=fake_cdp)
|
|
application.config["TESTING"] = True
|
|
return application
|
|
|
|
|
|
@pytest.fixture
|
|
def client(app):
|
|
return app.test_client()
|
|
|
|
|
|
def csrf_from(response) -> str:
|
|
match = re.search(r'name="csrf-token" content="([^"]+)"', response.get_data(as_text=True))
|
|
assert match, "csrf meta tag missing"
|
|
return match.group(1)
|
|
|
|
|
|
PASSWORD = "Correct-Paard-Batterij-42"
|
|
|
|
|
|
@pytest.fixture
|
|
def logged_in(app, client, paths):
|
|
"""A client with a configured admin password and an active session."""
|
|
app.extensions["voxip"].credentials.set_password(PASSWORD)
|
|
page = client.get("/login")
|
|
token = csrf_from(page)
|
|
resp = client.post("/login", data={"password": PASSWORD, "csrf_token": token})
|
|
assert resp.status_code == 302 and resp.headers["Location"].endswith("/")
|
|
return client
|
|
|
|
|
|
def token(client) -> str:
|
|
return csrf_from(client.get("/"))
|