diff --git a/_bmad-output/implementation-artifacts/1-2-configuration-read-write-wipe.md b/_bmad-output/implementation-artifacts/1-2-configuration-read-write-wipe.md index 639a9f0..6463462 100644 --- a/_bmad-output/implementation-artifacts/1-2-configuration-read-write-wipe.md +++ b/_bmad-output/implementation-artifacts/1-2-configuration-read-write-wipe.md @@ -1,6 +1,6 @@ # Story 1.2: Configuration Read/Write/Wipe -Status: ready-for-dev +Status: review ## Story @@ -20,34 +20,34 @@ So that all components share one reliable config boundary with no direct filesys ## Tasks / Subtasks -- [ ] Task 1: Implement `config.read()` in `src/planemapper/provisioning/config.py` (AC: #1, #4) - - [ ] 1.1 Import `CONFIG_PATH` from `planemapper.constants` - - [ ] 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] Task 1: Implement `config.read()` in `src/planemapper/provisioning/config.py` (AC: #1, #4) + - [x] 1.1 Import `CONFIG_PATH` from `planemapper.constants` + - [x] 1.2 Implement `read() -> dict` that opens `CONFIG_PATH` and parses JSON + - [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) - - [ ] 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 - - [ ] 2.3 Write JSON to `CONFIG_PATH` with `indent=2` +- [x] Task 2: Implement `config.write(data)` in `src/planemapper/provisioning/config.py` (AC: #2, #4) + - [x] 2.1 Implement `write(data: dict) -> None` + - [x] 2.2 Call `CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)` before writing to ensure `/etc/planemapper/` exists + - [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) - - [ ] 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] Task 3: Implement `config.wipe()` in `src/planemapper/provisioning/config.py` (AC: #3, #4) + - [x] 3.1 Implement `wipe() -> None` that deletes `CONFIG_PATH` if it exists + - [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) - - [ ] 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] Task 4: Update `tests/conftest.py` with `CONFIG_PATH` patch fixture (AC: #4) + - [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 + - [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) - - [ ] 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`) - - [ ] 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] Task 5: Write tests in `tests/provisioning/test_config.py` covering all 4 ACs (AC: #1, #2, #3, #4) + - [x] 5.1 Test AC1: call `config.read()` with no file present; assert `FileNotFoundError` is raised + - [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`) + - [x] 5.3 Test AC3: write a config, call `config.wipe()`, assert file is gone, then assert `config.read()` raises `FileNotFoundError` + - [x] 5.4 Test AC4: confirm `CONFIG_PATH` resolves inside `tmp_path` (not `/etc/planemapper/`) during test execution -- [ ] Task 6: Run quality gates - - [ ] 6.1 `pytest tests/` — all tests pass, 0 failures - - [ ] 6.2 `ruff check .` — zero violations - - [ ] 6.3 `ruff format --check .` — no formatting issues +- [x] Task 6: Run quality gates + - [x] 6.1 `pytest tests/` — all tests pass, 0 failures + - [x] 6.2 `ruff check .` — zero violations + - [x] 6.3 `ruff format --check .` — no formatting issues ## Dev Notes diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml index 2de4149..7ef1494 100644 --- a/_bmad-output/implementation-artifacts/sprint-status.yaml +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -45,7 +45,7 @@ development_status: # Epic 1: Device Setup & Provisioning epic-1: in-progress 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-4-location-resolution-icao-and-address: backlog 1-5-provisioning-execution-tile-download-cache-validation-and-wifi-kill: backlog diff --git a/pyproject.toml b/pyproject.toml index 511d36d..c537662 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,3 +40,4 @@ select = ["E", "F", "I", "TID", "UP"] "src/planemapper/provisioning/*.py" = ["TID251"] # Tests may import from provisioning to test its public API "tests/provisioning/*.py" = ["TID251"] +"tests/conftest.py" = ["TID251"] diff --git a/src/planemapper/provisioning/config.py b/src/planemapper/provisioning/config.py index d352c7e..6520826 100644 --- a/src/planemapper/provisioning/config.py +++ b/src/planemapper/provisioning/config.py @@ -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) diff --git a/tests/conftest.py b/tests/conftest.py index f9be49d..d2bf93f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,9 @@ from pathlib import Path import pytest +import planemapper.provisioning.config as config_module -@pytest.fixture -def tmp_config_path(tmp_path: Path) -> Path: - return tmp_path / "config.json" + +@pytest.fixture(autouse=True) +def patch_config_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(config_module, "CONFIG_PATH", tmp_path / "config.json") diff --git a/tests/provisioning/test_config.py b/tests/provisioning/test_config.py index b8bbd70..298a714 100644 --- a/tests/provisioning/test_config.py +++ b/tests/provisioning/test_config.py @@ -1,2 +1,69 @@ -def test_placeholder() -> None: - pass +import json +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))