Implement story 1.2: config read/write/wipe
Add provisioning/config.py with read/write/wipe functions, update conftest.py with autouse CONFIG_PATH patch fixture, write 7 tests covering all acceptance criteria, and extend pyproject.toml per-file-ignores so conftest.py may import from provisioning. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Story 1.2: Configuration Read/Write/Wipe
|
# Story 1.2: Configuration Read/Write/Wipe
|
||||||
|
|
||||||
Status: ready-for-dev
|
Status: review
|
||||||
|
|
||||||
## Story
|
## Story
|
||||||
|
|
||||||
@@ -20,34 +20,34 @@ So that all components share one reliable config boundary with no direct filesys
|
|||||||
|
|
||||||
## Tasks / Subtasks
|
## Tasks / Subtasks
|
||||||
|
|
||||||
- [ ] Task 1: Implement `config.read()` in `src/planemapper/provisioning/config.py` (AC: #1, #4)
|
- [x] Task 1: Implement `config.read()` in `src/planemapper/provisioning/config.py` (AC: #1, #4)
|
||||||
- [ ] 1.1 Import `CONFIG_PATH` from `planemapper.constants`
|
- [x] 1.1 Import `CONFIG_PATH` from `planemapper.constants`
|
||||||
- [ ] 1.2 Implement `read() -> dict` that opens `CONFIG_PATH` and parses JSON
|
- [x] 1.2 Implement `read() -> dict` that opens `CONFIG_PATH` and parses JSON
|
||||||
- [ ] 1.3 Let `FileNotFoundError` propagate naturally if the file does not exist — no bare `except`
|
- [x] 1.3 Let `FileNotFoundError` propagate naturally if the file does not exist — no bare `except`
|
||||||
|
|
||||||
- [ ] Task 2: Implement `config.write(data)` in `src/planemapper/provisioning/config.py` (AC: #2, #4)
|
- [x] Task 2: Implement `config.write(data)` in `src/planemapper/provisioning/config.py` (AC: #2, #4)
|
||||||
- [ ] 2.1 Implement `write(data: dict) -> None`
|
- [x] 2.1 Implement `write(data: dict) -> None`
|
||||||
- [ ] 2.2 Call `CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)` before writing to ensure `/etc/planemapper/` exists
|
- [x] 2.2 Call `CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)` before writing to ensure `/etc/planemapper/` exists
|
||||||
- [ ] 2.3 Write JSON to `CONFIG_PATH` with `indent=2`
|
- [x] 2.3 Write JSON to `CONFIG_PATH` with `indent=2`
|
||||||
|
|
||||||
- [ ] Task 3: Implement `config.wipe()` in `src/planemapper/provisioning/config.py` (AC: #3, #4)
|
- [x] Task 3: Implement `config.wipe()` in `src/planemapper/provisioning/config.py` (AC: #3, #4)
|
||||||
- [ ] 3.1 Implement `wipe() -> None` that deletes `CONFIG_PATH` if it exists
|
- [x] 3.1 Implement `wipe() -> None` that deletes `CONFIG_PATH` if it exists
|
||||||
- [ ] 3.2 Use `CONFIG_PATH.unlink(missing_ok=True)` so wipe is idempotent
|
- [x] 3.2 Use `CONFIG_PATH.unlink(missing_ok=True)` so wipe is idempotent
|
||||||
|
|
||||||
- [ ] Task 4: Update `tests/conftest.py` with `CONFIG_PATH` patch fixture (AC: #4)
|
- [x] Task 4: Update `tests/conftest.py` with `CONFIG_PATH` patch fixture (AC: #4)
|
||||||
- [ ] 4.1 Add a `monkeypatch` fixture (or autouse session fixture) that patches `planemapper.provisioning.config.CONFIG_PATH` to `tmp_path / "config.json"` for every test
|
- [x] 4.1 Add a `monkeypatch` fixture (or autouse session fixture) that patches `planemapper.provisioning.config.CONFIG_PATH` to `tmp_path / "config.json"` for every test
|
||||||
- [ ] 4.2 Confirm the fixture is applied so no test ever touches `/etc/planemapper/`
|
- [x] 4.2 Confirm the fixture is applied so no test ever touches `/etc/planemapper/`
|
||||||
|
|
||||||
- [ ] Task 5: Write tests in `tests/provisioning/test_config.py` covering all 4 ACs (AC: #1, #2, #3, #4)
|
- [x] Task 5: Write tests in `tests/provisioning/test_config.py` covering all 4 ACs (AC: #1, #2, #3, #4)
|
||||||
- [ ] 5.1 Test AC1: call `config.read()` with no file present; assert `FileNotFoundError` is raised
|
- [x] 5.1 Test AC1: call `config.read()` with no file present; assert `FileNotFoundError` is raised
|
||||||
- [ ] 5.2 Test AC2: call `config.write(data)` with a valid dict; assert file exists and JSON round-trips correctly with all expected keys (`home_lat`, `home_lon`, `coverage_radius_nm`, `wifi_ssid`, `wifi_password`, `provisioned`)
|
- [x] 5.2 Test AC2: call `config.write(data)` with a valid dict; assert file exists and JSON round-trips correctly with all expected keys (`home_lat`, `home_lon`, `coverage_radius_nm`, `wifi_ssid`, `wifi_password`, `provisioned`)
|
||||||
- [ ] 5.3 Test AC3: write a config, call `config.wipe()`, assert file is gone, then assert `config.read()` raises `FileNotFoundError`
|
- [x] 5.3 Test AC3: write a config, call `config.wipe()`, assert file is gone, then assert `config.read()` raises `FileNotFoundError`
|
||||||
- [ ] 5.4 Test AC4: confirm `CONFIG_PATH` resolves inside `tmp_path` (not `/etc/planemapper/`) during test execution
|
- [x] 5.4 Test AC4: confirm `CONFIG_PATH` resolves inside `tmp_path` (not `/etc/planemapper/`) during test execution
|
||||||
|
|
||||||
- [ ] Task 6: Run quality gates
|
- [x] Task 6: Run quality gates
|
||||||
- [ ] 6.1 `pytest tests/` — all tests pass, 0 failures
|
- [x] 6.1 `pytest tests/` — all tests pass, 0 failures
|
||||||
- [ ] 6.2 `ruff check .` — zero violations
|
- [x] 6.2 `ruff check .` — zero violations
|
||||||
- [ ] 6.3 `ruff format --check .` — no formatting issues
|
- [x] 6.3 `ruff format --check .` — no formatting issues
|
||||||
|
|
||||||
## Dev Notes
|
## Dev Notes
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ development_status:
|
|||||||
# Epic 1: Device Setup & Provisioning
|
# Epic 1: Device Setup & Provisioning
|
||||||
epic-1: in-progress
|
epic-1: in-progress
|
||||||
1-1-project-scaffold-and-verified-entry-points: done
|
1-1-project-scaffold-and-verified-entry-points: done
|
||||||
1-2-configuration-read-write-wipe: ready-for-dev
|
1-2-configuration-read-write-wipe: review
|
||||||
1-3-wifi-hotspot-and-captive-portal-form: backlog
|
1-3-wifi-hotspot-and-captive-portal-form: backlog
|
||||||
1-4-location-resolution-icao-and-address: backlog
|
1-4-location-resolution-icao-and-address: backlog
|
||||||
1-5-provisioning-execution-tile-download-cache-validation-and-wifi-kill: backlog
|
1-5-provisioning-execution-tile-download-cache-validation-and-wifi-kill: backlog
|
||||||
|
|||||||
@@ -40,3 +40,4 @@ select = ["E", "F", "I", "TID", "UP"]
|
|||||||
"src/planemapper/provisioning/*.py" = ["TID251"]
|
"src/planemapper/provisioning/*.py" = ["TID251"]
|
||||||
# Tests may import from provisioning to test its public API
|
# Tests may import from provisioning to test its public API
|
||||||
"tests/provisioning/*.py" = ["TID251"]
|
"tests/provisioning/*.py" = ["TID251"]
|
||||||
|
"tests/conftest.py" = ["TID251"]
|
||||||
|
|||||||
@@ -1 +1,22 @@
|
|||||||
# stub
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from planemapper.constants import CONFIG_PATH as _CONFIG_PATH
|
||||||
|
|
||||||
|
# Module-level reference so tests can monkeypatch this name
|
||||||
|
CONFIG_PATH: Path = _CONFIG_PATH
|
||||||
|
|
||||||
|
|
||||||
|
def read() -> dict:
|
||||||
|
with CONFIG_PATH.open() as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def write(data: dict) -> None:
|
||||||
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with CONFIG_PATH.open("w") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def wipe() -> None:
|
||||||
|
CONFIG_PATH.unlink(missing_ok=True)
|
||||||
|
|||||||
+5
-3
@@ -2,7 +2,9 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import planemapper.provisioning.config as config_module
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def tmp_config_path(tmp_path: Path) -> Path:
|
@pytest.fixture(autouse=True)
|
||||||
return tmp_path / "config.json"
|
def patch_config_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(config_module, "CONFIG_PATH", tmp_path / "config.json")
|
||||||
|
|||||||
@@ -1,2 +1,69 @@
|
|||||||
def test_placeholder() -> None:
|
import json
|
||||||
pass
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import planemapper.provisioning.config as config_module
|
||||||
|
|
||||||
|
SAMPLE_CONFIG = {
|
||||||
|
"home_lat": 51.5074,
|
||||||
|
"home_lon": -0.1278,
|
||||||
|
"coverage_radius_nm": 100,
|
||||||
|
"wifi_ssid": "HomeNetwork",
|
||||||
|
"wifi_password": "s3cr3t",
|
||||||
|
"provisioned": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_raises_when_no_file_exists() -> None:
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
config_module.read()
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_creates_file_with_correct_content(tmp_path: Path) -> None:
|
||||||
|
config_module.write(SAMPLE_CONFIG)
|
||||||
|
assert config_module.CONFIG_PATH.exists()
|
||||||
|
with config_module.CONFIG_PATH.open() as f:
|
||||||
|
data = json.load(f)
|
||||||
|
for key in (
|
||||||
|
"home_lat",
|
||||||
|
"home_lon",
|
||||||
|
"coverage_radius_nm",
|
||||||
|
"wifi_ssid",
|
||||||
|
"wifi_password",
|
||||||
|
"provisioned",
|
||||||
|
):
|
||||||
|
assert key in data
|
||||||
|
assert data["home_lat"] == 51.5074
|
||||||
|
assert data["wifi_ssid"] == "HomeNetwork"
|
||||||
|
assert data["provisioned"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_then_read_round_trips() -> None:
|
||||||
|
config_module.write(SAMPLE_CONFIG)
|
||||||
|
result = config_module.read()
|
||||||
|
assert result == SAMPLE_CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
def test_wipe_deletes_file() -> None:
|
||||||
|
config_module.write(SAMPLE_CONFIG)
|
||||||
|
assert config_module.CONFIG_PATH.exists()
|
||||||
|
config_module.wipe()
|
||||||
|
assert not config_module.CONFIG_PATH.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_wipe_then_read_raises() -> None:
|
||||||
|
config_module.write(SAMPLE_CONFIG)
|
||||||
|
config_module.wipe()
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
config_module.read()
|
||||||
|
|
||||||
|
|
||||||
|
def test_wipe_is_idempotent() -> None:
|
||||||
|
# wipe on a non-existent file should not raise
|
||||||
|
config_module.wipe()
|
||||||
|
config_module.wipe()
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_path_is_in_tmp_path(tmp_path: Path) -> None:
|
||||||
|
assert str(config_module.CONFIG_PATH).startswith(str(tmp_path))
|
||||||
|
|||||||
Reference in New Issue
Block a user