Implement story 1.5: provisioning execution tile download cache validation wifi kill

Add tiles.py, airspace.py, wifi.join_home_wifi, portal /submit route, and rewire
provision.py main loop; all tasks and quality gates pass (56 tests, ruff clean).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Edholm
2026-04-22 22:54:34 -04:00
parent a6a6a2796d
commit 4aeeefb488
11 changed files with 455 additions and 81 deletions
+31
View File
@@ -0,0 +1,31 @@
import json
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from planemapper.provisioning.airspace import download
def test_download_no_api_key(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
airspace_path = tmp_path / "airspace.geojson"
monkeypatch.delenv("OPENAIP_API_KEY", raising=False)
with patch("planemapper.provisioning.airspace.AIRSPACE_PATH", airspace_path):
download(51.5, -0.1, 100)
data = json.loads(airspace_path.read_text())
assert data["type"] == "FeatureCollection"
assert data["features"] == []
def test_download_with_api_key(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
airspace_path = tmp_path / "airspace.geojson"
monkeypatch.setenv("OPENAIP_API_KEY", "testkey123")
mock_resp = MagicMock()
mock_resp.ok = True
mock_resp.text = '{"type":"FeatureCollection","features":[]}'
with (
patch("planemapper.provisioning.airspace.requests.get", return_value=mock_resp),
patch("planemapper.provisioning.airspace.AIRSPACE_PATH", airspace_path),
):
download(51.5, -0.1, 100)
assert airspace_path.exists()
+52
View File
@@ -2,6 +2,7 @@ from unittest.mock import patch
import pytest
from planemapper.provisioning import ProvisioningError
from planemapper.provisioning.portal import app
@@ -69,3 +70,54 @@ def test_find_location_error(client) -> None:
resp = client.post("/find-location", data={"location": "ZZZZ", "radius": "100"})
assert resp.status_code == 200
assert "ICAO code not found" in resp.data.decode()
def test_submit_success(client) -> None:
with (
patch("planemapper.provisioning.portal.wifi.join_home_wifi"),
patch("planemapper.provisioning.portal.tiles.download_and_composite"),
patch("planemapper.provisioning.portal.airspace.download"),
patch("planemapper.provisioning.portal.tiles.validate_cache"),
patch("planemapper.provisioning.portal.config.write"),
patch("planemapper.provisioning.portal.wifi.kill_wifi"),
):
resp = client.post(
"/submit",
data={
"confirmed_lat": "51.5",
"confirmed_lon": "-0.1",
"confirmed_name": "London",
"radius": "100",
"wifi_ssid": "HomeNet",
"wifi_password": "secret",
},
)
assert resp.status_code == 200
assert b"Setup complete" in resp.data
def test_submit_validation_failure(client) -> None:
with (
patch("planemapper.provisioning.portal.wifi.join_home_wifi"),
patch("planemapper.provisioning.portal.tiles.download_and_composite"),
patch("planemapper.provisioning.portal.airspace.download"),
patch(
"planemapper.provisioning.portal.tiles.validate_cache",
side_effect=ProvisioningError("bad png"),
),
patch("planemapper.provisioning.portal.config.write"),
patch("planemapper.provisioning.portal.wifi.kill_wifi"),
):
resp = client.post(
"/submit",
data={
"confirmed_lat": "51.5",
"confirmed_lon": "-0.1",
"confirmed_name": "London",
"radius": "100",
"wifi_ssid": "HomeNet",
"wifi_password": "secret",
},
)
assert resp.status_code == 200
assert b"Try again" in resp.data
+71 -2
View File
@@ -1,2 +1,71 @@
def test_placeholder() -> None:
pass
import io
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from PIL import Image
from planemapper.provisioning import ProvisioningError
from planemapper.provisioning.tiles import (
download_and_composite,
lat_lon_to_tile,
validate_cache,
)
def _make_png_bytes() -> bytes:
img = Image.new("RGB", (256, 256), (200, 200, 200))
buf = io.BytesIO()
img.save(buf, format="PNG")
return buf.getvalue()
def test_lat_lon_to_tile_london() -> None:
x, y = lat_lon_to_tile(51.5, -0.1, 10)
assert x == 511
assert y == 340
def test_download_and_composite(tmp_path: Path) -> None:
bg_path = tmp_path / "background.png"
mock_resp = MagicMock()
mock_resp.content = _make_png_bytes()
mock_resp.raise_for_status = MagicMock()
with (
patch("planemapper.provisioning.tiles.requests.get", return_value=mock_resp),
patch("planemapper.provisioning.tiles.BACKGROUND_PATH", bg_path),
):
download_and_composite(51.5, -0.1, 100)
assert bg_path.exists()
with Image.open(bg_path) as img:
assert img.size == (800, 480)
def test_validate_cache_passes(tmp_path: Path) -> None:
bg_path = tmp_path / "background.png"
Image.new("RGB", (800, 480), (255, 255, 255)).save(str(bg_path))
with patch("planemapper.provisioning.tiles.BACKGROUND_PATH", bg_path):
validate_cache()
def test_validate_cache_missing(tmp_path: Path) -> None:
bg_path = tmp_path / "nonexistent.png"
with patch("planemapper.provisioning.tiles.BACKGROUND_PATH", bg_path):
with pytest.raises(ProvisioningError, match="not found"):
validate_cache()
def test_validate_cache_empty(tmp_path: Path) -> None:
bg_path = tmp_path / "background.png"
bg_path.write_bytes(b"")
with patch("planemapper.provisioning.tiles.BACKGROUND_PATH", bg_path):
with pytest.raises(ProvisioningError, match="empty"):
validate_cache()
def test_validate_cache_corrupt(tmp_path: Path) -> None:
bg_path = tmp_path / "background.png"
bg_path.write_bytes(b"not a png at all")
with patch("planemapper.provisioning.tiles.BACKGROUND_PATH", bg_path):
with pytest.raises(ProvisioningError, match="not a valid PNG"):
validate_cache()
+22
View File
@@ -0,0 +1,22 @@
from unittest.mock import MagicMock, patch
import pytest
from planemapper.provisioning import ProvisioningError
from planemapper.provisioning.wifi import join_home_wifi
def test_join_home_wifi_success() -> None:
mock_result = MagicMock()
mock_result.returncode = 0
with patch("planemapper.provisioning.wifi.subprocess.run", return_value=mock_result):
join_home_wifi("MySSID", "MyPass")
def test_join_home_wifi_failure() -> None:
mock_result = MagicMock()
mock_result.returncode = 1
mock_result.stderr = b"Error: connection failed"
with patch("planemapper.provisioning.wifi.subprocess.run", return_value=mock_result):
with pytest.raises(ProvisioningError, match="nmcli failed"):
join_home_wifi("MySSID", "wrongpassword")