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:
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user