Create story 1.2: configuration read/write/wipe
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
# Story 1.2: Configuration Read/Write/Wipe
|
||||||
|
|
||||||
|
Status: ready-for-dev
|
||||||
|
|
||||||
|
## Story
|
||||||
|
|
||||||
|
As a provisioning system,
|
||||||
|
I want a single config module that reads, writes, and wipes `/etc/planemapper/config.json`,
|
||||||
|
So that all components share one reliable config boundary with no direct filesystem access elsewhere.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
1. **Given** no config file exists at `CONFIG_PATH` **When** `config.read()` is called **Then** it raises `FileNotFoundError`
|
||||||
|
|
||||||
|
2. **Given** a valid config dict with home lat/lon, coverage radius, WiFi SSID/password, and `provisioned` flag **When** `config.write(data)` is called **Then** the file is created at `CONFIG_PATH` with correct JSON content and all expected keys present
|
||||||
|
|
||||||
|
3. **Given** an existing config file **When** `config.wipe()` is called **Then** the config file is deleted and a subsequent `config.read()` raises `FileNotFoundError`
|
||||||
|
|
||||||
|
4. **Given** a test using `conftest.py` **When** `CONFIG_PATH` is patched to `tmp_path` **Then** all config operations work without touching `/etc/planemapper/`
|
||||||
|
|
||||||
|
## 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`
|
||||||
|
|
||||||
|
- [ ] 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`
|
||||||
|
|
||||||
|
- [ ] 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
|
||||||
|
|
||||||
|
- [ ] 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/`
|
||||||
|
|
||||||
|
- [ ] 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
|
||||||
|
|
||||||
|
- [ ] 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
|
||||||
|
|
||||||
|
## Dev Notes
|
||||||
|
|
||||||
|
### CONFIG_PATH patching
|
||||||
|
`CONFIG_PATH` is a module-level `Path` constant defined in `planemapper.constants` and imported into `planemapper.provisioning.config`. Patch the name **in the config module's namespace** — `monkeypatch.setattr("planemapper.provisioning.config.CONFIG_PATH", tmp_path / "config.json")` — not in `planemapper.constants`, so that all three functions pick up the patched value.
|
||||||
|
|
||||||
|
### Directory creation in `config.write()`
|
||||||
|
The directory `/etc/planemapper/` will not exist in CI or on a fresh OS install. `config.write()` must call `CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)` before opening the file for writing. In tests, `tmp_path` already exists, so this call is a safe no-op.
|
||||||
|
|
||||||
|
### JSON formatting
|
||||||
|
Write config files with `json.dump(data, f, indent=2)` for human readability. No extra dependencies — use the Python stdlib `json` module only.
|
||||||
|
|
||||||
|
### Error handling policy
|
||||||
|
Do not catch any exceptions inside `read()`, `write()`, or `wipe()`. Let `FileNotFoundError`, `PermissionError`, `json.JSONDecodeError`, etc. propagate to callers. Provisioning code higher up the stack is responsible for user-facing error messages.
|
||||||
|
|
||||||
|
### Config schema
|
||||||
|
The canonical key set written by `config.write()` and expected by all consumers:
|
||||||
|
|
||||||
|
```
|
||||||
|
home_lat float — decimal degrees, WGS-84
|
||||||
|
home_lon float — decimal degrees, WGS-84
|
||||||
|
coverage_radius_nm int — display radius in nautical miles
|
||||||
|
wifi_ssid str — WiFi network name
|
||||||
|
wifi_password str — WiFi passphrase
|
||||||
|
provisioned bool — True once provisioning has completed
|
||||||
|
```
|
||||||
|
|
||||||
|
### File location constants
|
||||||
|
`CONFIG_PATH = Path("/etc/planemapper/config.json")` is defined in `src/planemapper/constants.py`. Do not hard-code the path string anywhere in `config.py` or test files; always reference the module-level `CONFIG_PATH` symbol so the monkeypatch can redirect it cleanly.
|
||||||
@@ -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: backlog
|
1-2-configuration-read-write-wipe: ready-for-dev
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user