wrolpi/controller/test/test_defaults.py

134 lines
5.2 KiB
Python
Raw Normal View History

"""
Unit tests for controller.defaults module.
"""
import pytest
from controller.defaults import DEFAULT_CONFIG
class TestDefaultConfig:
"""Tests for DEFAULT_CONFIG structure."""
def test_default_config_is_dict(self):
"""DEFAULT_CONFIG should be a dictionary."""
assert isinstance(DEFAULT_CONFIG, dict)
def test_default_port(self):
"""Default port should be 80."""
assert DEFAULT_CONFIG["port"] == 80
def test_default_media_directory(self):
"""Default media directory should be /media/wrolpi."""
assert DEFAULT_CONFIG["media_directory"] == "/media/wrolpi"
class TestDrivesConfig:
"""Tests for drives configuration."""
def test_drives_config_exists(self):
"""Drives config should exist."""
assert "drives" in DEFAULT_CONFIG
assert isinstance(DEFAULT_CONFIG["drives"], dict)
@pytest.mark.parametrize("filesystem", ["ext4", "btrfs", "vfat", "exfat"])
def test_supported_filesystem(self, filesystem):
"""Should have expected filesystems in supported list."""
filesystems = DEFAULT_CONFIG["drives"]["supported_filesystems"]
assert isinstance(filesystems, list)
assert filesystem in filesystems
def test_auto_mount_default(self):
"""Auto mount should be True by default."""
assert DEFAULT_CONFIG["drives"]["auto_mount"] is True
def test_mounts_empty_by_default(self):
"""Mounts list should be empty by default."""
assert DEFAULT_CONFIG["drives"]["mounts"] == []
class TestManagedServicesConfig:
"""Tests for managed services configuration."""
def test_managed_services_exists(self):
"""Managed services config should exist."""
assert "managed_services" in DEFAULT_CONFIG
assert isinstance(DEFAULT_CONFIG["managed_services"], list)
def test_managed_services_count(self):
"""Should have expected number of managed services."""
services = DEFAULT_CONFIG["managed_services"]
assert len(services) == 12
@pytest.mark.parametrize("name,expected", [
("wrolpi-api",
{"systemd_name": "wrolpi-api", "port": 8081, "viewable": True, "description": "Python API (Sanic)"}),
("wrolpi-api-dev",
{"systemd_name": "wrolpi-api-dev", "port": 8081, "viewable": True, "show_only_when_running": True}),
("wrolpi-app", {"port": 3000, "viewable": False}),
("wrolpi-app-dev", {"port": 3000, "viewable": False, "show_only_when_running": True}),
("caddy", {"port": 443}),
("wrolpi-upgrade", {"port": None, "viewable": False, "show_only_when_running": True}),
("wrolpi-llm",
{"systemd_name": "wrolpi-llm", "port": 11435, "viewable": False, "kind": "persistent"}),
])
def test_service_config(self, name, expected):
"""Should have service configured with expected values."""
services = DEFAULT_CONFIG["managed_services"]
service = next((s for s in services if s["name"] == name), None)
assert service is not None, f"Service {name} not found"
for key, value in expected.items():
assert service[key] == value, f"Service {name} has {key}={service.get(key)}, expected {value}"
@pytest.mark.parametrize("name,use_https", [
("wrolpi-help", True),
("wrolpi-kiwix", True),
("wrolpi-api", False),
("wrolpi-app", False),
])
def test_service_use_https(self, name, use_https):
"""HTTPS services should have use_https=True, others should have False."""
services = DEFAULT_CONFIG["managed_services"]
service = next((s for s in services if s["name"] == name), None)
assert service is not None, f"Service {name} not found"
assert service.get("use_https", False) == use_https, \
f"Service {name} should have use_https={use_https}"
def test_all_services_have_required_fields(self):
"""All services should have required fields."""
required_fields = ["name", "systemd_name", "port", "viewable", "use_https", "description"]
for service in DEFAULT_CONFIG["managed_services"]:
for field in required_fields:
assert field in service, f"Service {service.get('name')} missing {field}"
class TestHotspotConfig:
"""Tests for hotspot configuration."""
def test_hotspot_config_exists(self):
"""Hotspot config should exist."""
assert "hotspot" in DEFAULT_CONFIG
assert isinstance(DEFAULT_CONFIG["hotspot"], dict)
@pytest.mark.parametrize("key,expected", [
("device", "wlan0"),
("ssid", "WROLPi"),
("password", "wrolpi hotspot"),
Hotspot captive portal: welcome page with the WROLPi's address (#709) * Hotspot captive portal: welcome page with the WROLPi's address Devices that join the hotspot probe a fixed plain-HTTP URL to check for Internet access. NetworkManager's shared-mode dnsmasq now resolves those probe hostnames (and `wrolpi`) to the hotspot address via an include the Controller writes when the hotspot starts, the probes land on the Controller (port 80), and are redirected to /portal: "This is your WROLPi, its address is 10.42.0.1, open WROLPi / open the Controller". Only the probe hostnames are hijacked so the hotspot keeps forwarding everything else when the WROLPi has an uplink. The include is corrected and the connection re-activated once if NetworkManager picked a non-default subnet. Writing the include is best-effort; the hotspot starts regardless. `hotspot.captive_portal` (controller.yaml, default true) toggles the probe redirects and the include; like the other hotspot settings it applies on the next hotspot start, so saving while the hotspot is up reports restart_required and both Controller UIs say so. The status endpoint gains the hotspot address and portal URL, shown in both UIs. repair.sh only creates the include directory so a disabled setting survives upgrades. Native only: Docker has no hotspot. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * captive portal: address review * React: re-read the hotspot address whenever the HotspotToggle starts or stops the hotspot, matching the fallback UI's refresh-on-status. * A WPA3 start that reverts to WPA2 leaves the hotspot up, so it now gets the captive portal address correction too. * The corrective `nmcli connection up` logs a warning on a non-zero exit; dnsmasq would otherwise keep the old address silently. * HotspotStatusResponse.captive_portal defaults to true like everywhere else. * Trim design narrative from the module docstring and React comments. * The include-before-activate test now observes the include at the moment of the `nmcli ... hotspot` call instead of inferring from call order. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * captive portal: never forward any record type for probe hosts `address=` only synthesizes A/AAAA answers. iOS also asks for the HTTPS record type; dnsmasq forwarded that upstream, Apple's answer aliased to captive.g.aaplimg.com, and the phone fetched the genuine "Success" from Apple instead of the portal. Each probe host now also gets `local=`, so dnsmasq answers every type itself, and the CDN alias is a probe host too. Observed with dnsmasq query logging on a hotspot with an eth0 uplink. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * captive portal: let acknowledged clients finish so the OS offers "Done" A phone's sign-in sheet only shows "Done" once the OS re-probes and gets the genuine success reply. The Controller now remembers each client (by hotspot IP) that has loaded the portal page and answers its later probes with the reply each OS expects (Apple "Success" HTML, Android 204, Windows "Microsoft Connect Test", ...). The set is cleared when the hotspot starts so a new session shows the page again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * captive portal: acknowledge on Continue, not on view iOS fetches the portal page in the background before opening the sheet, then re-probes after the sheet's own navigation. Acknowledging on view made that re-probe return "Success" as the sheet's content. Phones only re-check the network (and offer "Done") after a full-page navigation, so the welcome page now has a Continue button leading to /portal/continue, which is the only place a client is acknowledged. The Continue page says to tap Done and links to WROLPi and the Controller. Also hijack clients3.google.com (older Android probe host). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * help: bump wrolpi-help to the captive portal and Speed Test docs Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * captive portal: comments describe the Continue design; clear acks on stop Three comments still described acknowledging on view, which c72a7f7b removed. Acknowledged clients are now also cleared when the hotspot stops, so a lease reused in a later session sees the portal page. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 20:09:48 -06:00
("captive_portal", True),
])
def test_hotspot_defaults(self, key, expected):
"""Hotspot should have expected default values."""
assert DEFAULT_CONFIG["hotspot"][key] == expected
class TestThrottleConfig:
"""Tests for throttle configuration."""
def test_throttle_config_exists(self):
"""Throttle config should exist."""
assert "throttle" in DEFAULT_CONFIG
assert isinstance(DEFAULT_CONFIG["throttle"], dict)
def test_default_governor(self):
"""Default governor should be ondemand."""
assert DEFAULT_CONFIG["throttle"]["default_governor"] == "ondemand"