VoxIP Kiosk OS 1.0.0: Raspberry Pi kiosk with web management
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>
This commit is contained in:
commit
a33c4822bf
96 files changed
+7535
No files matched your search
@@ -0,0 +1,142 @@
|
||||
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("/"))
|
||||
Reference in new issue
Block a user