Implement story 1.3: WiFi hotspot and captive portal form
Implement wifi.py (start_ap/stop_ap/kill_wifi with subprocess and ProvisioningError), portal.py (Flask captive portal with Android/iOS/Windows probe redirects and 404 catch-all), updated provision.py provisioning loop, and full test suite (38 passing). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import pytest
|
||||
|
||||
from planemapper.provisioning.portal import app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app.config["TESTING"] = True
|
||||
with app.test_client() as c:
|
||||
yield c
|
||||
|
||||
|
||||
def test_index_returns_200(client) -> None:
|
||||
resp = client.get("/")
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
def test_index_contains_form_fields(client) -> None:
|
||||
resp = client.get("/")
|
||||
data = resp.data.decode()
|
||||
assert 'name="location"' in data
|
||||
assert 'name="radius"' in data
|
||||
assert 'name="wifi_ssid"' in data
|
||||
assert 'name="wifi_password"' in data
|
||||
assert "Find location" in data
|
||||
assert "Set up device" in data
|
||||
|
||||
|
||||
def test_generate_204_redirects_to_index(client) -> None:
|
||||
resp = client.get("/generate_204")
|
||||
assert resp.status_code in (301, 302)
|
||||
assert resp.headers["Location"].endswith("/")
|
||||
|
||||
|
||||
def test_hotspot_detect_redirects_to_index(client) -> None:
|
||||
resp = client.get("/hotspot-detect.html")
|
||||
assert resp.status_code in (301, 302)
|
||||
|
||||
|
||||
def test_ncsi_redirects_to_index(client) -> None:
|
||||
resp = client.get("/ncsi.txt")
|
||||
assert resp.status_code in (301, 302)
|
||||
|
||||
|
||||
def test_unknown_route_redirects_to_index(client) -> None:
|
||||
resp = client.get("/some/random/path")
|
||||
assert resp.status_code in (301, 302)
|
||||
@@ -1,4 +1,8 @@
|
||||
from planemapper.provisioning import ProvisioningError
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from planemapper.provisioning import ProvisioningError, wifi
|
||||
|
||||
|
||||
def test_provisioning_error_is_exception() -> None:
|
||||
@@ -10,3 +14,41 @@ def test_provisioning_error_can_be_raised_and_caught() -> None:
|
||||
raise ProvisioningError("test error")
|
||||
except ProvisioningError as e:
|
||||
assert str(e) == "test error"
|
||||
|
||||
|
||||
def test_start_ap_raises_on_hostapd_failure() -> None:
|
||||
mock_fail = MagicMock()
|
||||
mock_fail.returncode = 1
|
||||
with patch("planemapper.provisioning.wifi._write_hostapd_conf"):
|
||||
with patch("planemapper.provisioning.wifi.subprocess.run", return_value=mock_fail):
|
||||
with pytest.raises(ProvisioningError, match="hostapd failed"):
|
||||
wifi.start_ap()
|
||||
|
||||
|
||||
def test_start_ap_raises_on_dnsmasq_failure() -> None:
|
||||
mock_ok = MagicMock()
|
||||
mock_ok.returncode = 0
|
||||
mock_fail = MagicMock()
|
||||
mock_fail.returncode = 1
|
||||
with patch("planemapper.provisioning.wifi._write_hostapd_conf"):
|
||||
with patch(
|
||||
"planemapper.provisioning.wifi.subprocess.run",
|
||||
side_effect=[mock_ok, mock_fail], # hostapd OK, dnsmasq fails
|
||||
):
|
||||
with pytest.raises(ProvisioningError, match="dnsmasq failed"):
|
||||
wifi.start_ap()
|
||||
|
||||
|
||||
def test_kill_wifi_raises_on_rfkill_failure() -> None:
|
||||
mock_fail = MagicMock()
|
||||
mock_fail.returncode = 1
|
||||
with patch("planemapper.provisioning.wifi.subprocess.run", return_value=mock_fail):
|
||||
with pytest.raises(ProvisioningError, match="rfkill failed"):
|
||||
wifi.kill_wifi()
|
||||
|
||||
|
||||
def test_stop_ap_does_not_raise() -> None:
|
||||
mock_ok = MagicMock()
|
||||
mock_ok.returncode = 0
|
||||
with patch("planemapper.provisioning.wifi.subprocess.run", return_value=mock_ok):
|
||||
wifi.stop_ap() # should not raise
|
||||
|
||||
Reference in New Issue
Block a user