upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/PLAN_pytest_migration.md
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-23 02:06:10 +0530
committerYour Name <you@example.com>2026-05-23 02:06:10 +0530
commit969dfe9ea52ed304ae44d7f7adbc5219f90497dd (patch)
tree127481ab6a6e4a963e4ac330051cecf57a40f511 /PLAN_pytest_migration.md
parent0c95564af9ba2ab772b5269970d0153c9a59c36f (diff)
pytests pass, now proceeding to spearate out tollgate core
Diffstat (limited to 'PLAN_pytest_migration.md')
-rw-r--r--PLAN_pytest_migration.md284
1 files changed, 284 insertions, 0 deletions
diff --git a/PLAN_pytest_migration.md b/PLAN_pytest_migration.md
new file mode 100644
index 0000000..dfea3d9
--- /dev/null
+++ b/PLAN_pytest_migration.md
@@ -0,0 +1,284 @@
1# ESP32 pytest Migration Plan
2
3Migrate ESP32 board integration tests from Node.js `.mjs` + Makefile to pytest with proper fixtures for board provisioning (mutex locks, flashing, SPIFFS config, teardown).
4
5## Architecture
6
7### New Files
8
9| File | Location | Purpose |
10|------|----------|---------|
11| `esp32_board.py` | `lib/` | `ESP32Board` class: lock, flash, config, reset, HTTP API, wait_for_boot |
12| `conftest.py` | `tests/esp32/` | Board fixtures (`board_a`, `board_b`, `board_c`), marker registration, `.env` loading |
13| `pytest-esp32.ini` | project root | Separate pytest config for ESP32 tests |
14
15### Migrated Tests
16
17| Priority | Source (.mjs) | Target (pytest) | Payment? | Boards |
18|----------|--------------|-----------------|----------|--------|
19| 1 | `tests/integration/api.mjs` | `tests/esp32/test_api.py` | No | a |
20| 2 | `tests/integration/smoke.mjs` | `tests/esp32/test_smoke.py` | No | a |
21| 3 | `tests/integration/test-cvm-roundtrip.mjs` | `tests/esp32/test_cvm.py` | No | a |
22| 4 | `tests/integration/test-local-relay.mjs` | `tests/esp32/test_local_relay.py` | No | a |
23| 5 | `tests/integration/test-relay-nip11.mjs` | `tests/esp32/test_relay_nip11.py` | No | a |
24| 6 | `tests/integration/test-market.mjs` | `tests/esp32/test_market.py` | No | a |
25| 7 | `tests/integration/test-cross-board.mjs` | `tests/esp32/test_cross_board.py` | No | a, c |
26| 8 | `tests/integration/test-reset-auth.mjs` | `tests/esp32/test_reset_auth.py` | Yes | a |
27| 9 | `tests/integration/test-session-expiry.mjs` | `tests/esp32/test_session_expiry.py` | Yes | a |
28| 10 | `tests/integration/test-dns-firewall.mjs` | `tests/esp32/test_dns_firewall.py` | Yes | a |
29
30All paths are relative to `/home/c03rad0r/physical-router-test-automation/`.
31
32---
33
34## `lib/esp32_board.py` — ESP32Board Helper
35
36```python
37class ESP32Board:
38 """ESP32 board: lock, flash, SPIFFS config, reset, HTTP API access."""
39
40 LOCK_DIR = "/home/c03rad0r/physical-router-test-automation/locks"
41 SPIFFS_OFFSET = 0x410000
42 SPIFFS_SIZE = 0xF0000
43 IDF_PATH = os.environ.get("IDF_PATH", os.path.expanduser("~/esp/esp-idf"))
44 SPIFFSGEN = f"{IDF_PATH}/components/spiffs/spiffsgen.py"
45
46 def __init__(self, board_id, port, nsec, ssid, ip, worktree):
47 self.board_id = board_id # "a", "b", "c"
48 self.port = port # "/dev/ttyACM0"
49 self.nsec = nsec
50 self.ssid = ssid # derived from nsec via HMAC-SHA512
51 self.ip = ip # derived AP IP
52 self.worktree = worktree # /home/c03rad0r/esp32-tollgate
53 self._lock_held = False
54
55 # --- Lock (board-{id}.lock, same as Makefile) ---
56 def acquire_lock(self, phase="pytest"): ...
57 def release_lock(self): ...
58
59 # --- Firmware ---
60 def build(self): ... # idf.py build (in worktree)
61 def flash(self, baud=460800): ... # build + idf.py -p {port} flash
62
63 # --- SPIFFS Config ---
64 def write_config(self, wifi_ssid, wifi_password, mint_url, **extra): ...
65 def write_config_ap_only(self, **extra): ...
66 # 1. mktemp -d
67 # 2. Write config.json with nsec, wifi_networks, mint_url, price, relays
68 # 3. python3 spiffsgen.py --page-size 256 --obj-name-len 32 \
69 # --use-magic --use-magic-len 0xF0000 tmpdir/ tmpdir/spiffs.bin
70 # 4. esptool --port {port} --baud {baud} write_flash 0x410000 spiffs.bin
71 # 5. esptool --port {port} run (reset)
72
73 # --- Reset ---
74 def reset(self): ... # esptool --port {port} run
75
76 # --- Wait ---
77 def wait_for_boot(self, timeout=30): ... # poll http://{ip}:2121/
78 def wait_for_ap(self, timeout=30): ... # poll ping {ip}
79
80 # --- HTTP API (port 2121) ---
81 def api_get(self, path="/") -> (int, str): ...
82 def api_post(self, path, data="") -> (int, str): ...
83
84 # --- Captive Portal (port 80) ---
85 def http_get(self, path="/") -> (int, str): ...
86
87 # --- WiFi client (Linux nmcli) ---
88 def connect_wifi(self, interface="wlp59s0"): ...
89 def disconnect_wifi(self, interface="wlp59s0"): ...
90
91 # --- Context manager ---
92 def __enter__(self): return self
93 def __exit__(self, *exc): self.release_lock()
94```
95
96## `pytest-esp32.ini`
97
98```ini
99[pytest]
100pythonpath = .
101testpaths = tests/esp32
102python_files = test_*.py
103python_functions = test_*
104markers =
105 boards(*ids): specify which ESP32 boards to provision
106 api: API-level test (HTTP only, no browser)
107 smoke: quick connectivity check
108 payment: requires mint/Cashu token
109 cvm: ContextVM MCP test
110 relay: Nostr relay test
111 cross_board: requires multiple boards
112addopts = -v --tb=short --timeout=120 --timeout-method=thread
113```
114
115## `tests/esp32/conftest.py` — Fixtures
116
117```python
118# Loads .env from /home/c03rad0r/esp32-tollgate/.env
119# Board registry maps board_id -> env var names
120
121BOARD_REGISTRY = {
122 "a": dict(port_env="PORT_A", nsec_env="BOARD_A_NSEC",
123 ssid_env="BOARD_A_SSID", ip_env="BOARD_A_IP"),
124 "b": dict(port_env="PORT_B", nsec_env="BOARD_B_NSEC",
125 ssid_env="BOARD_B_SSID", ip_env="BOARD_B_IP"),
126 "c": dict(port_env="PORT_C", nsec_env="BOARD_C_NSEC",
127 ssid_env="BOARD_C_SSID", ip_env="BOARD_C_IP"),
128}
129
130# CLI options
131--esp32-no-flash Skip firmware flash (assume already flashed)
132--esp32-no-config Skip SPIFFS config write
133--esp32-worktree Path to esp32-tollgate (default: /home/c03rad0r/esp32-tollgate)
134
135# Fixtures (session-scoped, lazy provisioning)
136@pytest.fixture(scope="session")
137def board_a(request): ...
138 # 1. acquire_lock
139 # 2. flash (unless --esp32-no-flash)
140 # 3. write_config (unless --esp32-no-config)
141 # 4. wait_for_boot
142 # 5. yield board
143 # 6. release_lock
144
145@pytest.fixture(scope="session")
146def board_b(request): ... # same pattern
147
148@pytest.fixture(scope="session")
149def board_c(request): ... # same pattern
150
151@pytest.fixture(scope="session")
152def cashu_esp32(): ... # CashuMint for test mint
153```
154
155## Marker-based Board Selection
156
157Tests declare which boards they need via `@pytest.mark.boards("a", "c")`.
158Only those board fixtures are provisioned. Tests requesting boards not
159available in the session are skipped.
160
161```python
162# Single board
163pytestmark = [pytest.mark.api, pytest.mark.boards("a")]
164
165# Multiple boards
166pytestmark = [pytest.mark.cross_board, pytest.mark.boards("a", "c")]
167```
168
169## Setup/Teardown Lifecycle
170
171```
172pytest -c pytest-esp32.ini
173 |
174 +-- pytest_sessionstart: (board fixtures are lazy)
175 |
176 +-- First test requests board_a:
177 | +-- acquire_lock("board-a") -> board-a.lock
178 | +-- flash() -> idf.py build + flash (~60s)
179 | +-- write_config() -> spiffsgen + esptool write_flash + reset (~5s)
180 | +-- wait_for_boot() -> poll :2121 until 200 (~10s)
181 | +-- yield board_a
182 |
183 +-- Tests run using board_a.api_get(), board_a.http_get(), etc.
184 |
185 +-- pytest_sessionfinish:
186 | +-- board_a.release_lock() -> remove board-a.lock
187 |
188 +-- Done
189```
190
191## CLI Usage
192
193```bash
194# Run all ESP32 tests (flash + configure by default)
195pytest -c pytest-esp32.ini
196
197# Skip flash (faster, assumes firmware already on board)
198pytest -c pytest-esp32.ini --esp32-no-flash
199
200# Skip config too (board already configured)
201pytest -c pytest-esp32.ini --esp32-no-flash --esp32-no-config
202
203# Run only smoke tests
204pytest -c pytest-esp32.ini -m smoke
205
206# Run only cross-board tests (needs boards a + c)
207pytest -c pytest-esp32.ini -m cross_board
208
209# Single test file
210pytest -c pytest-esp32.ini tests/esp32/test_api.py
211
212# Verbose with no timeout (debugging)
213pytest -c pytest-esp32.ini tests/esp32/test_api.py -v --timeout=0
214```
215
216## Key Design Decisions
217
2181. **Flash by default** — guarantees known-good firmware state per session
2192. **Merged into existing `esp32/` test suite** — removed duplicate `tests/esp32/`, added new tests to `esp32/tests/`
2203. **Same lock files as Makefile** — `board-{a,b,c}.lock` in `physical-router-test-automation/locks/`, mutually exclusive with `make flash-a` etc.
2214. **Board C support** — `esp32/boards.env` and `esp32/boards.py` support all three boards
2225. **Session-scoped `board_connected`** — connects WiFi once per session
2236. **`--board` flag** — `pytest --board=a` selects board via marker filtering
2247. **Test ordering matters** — smoke tests first (they're self-contained), then API (some reset auth), then relay
2258. **Existing fixtures** — `board_connected`, `http`, `wifi` from `esp32/conftest.py`; added `relay`, `dns` helpers
226
227## Board Identity Reference
228
229| Board | Port | SSID | AP IP | nsec (first 8) |
230|-------|------|------|-------|-----------------|
231| A | `/dev/ttyACM2` | `TollGate-B96D80` | `10.185.47.1` | `9af47906...` |
232| B | `/dev/ttyACM1` | `TollGate-C0E9CA` | `10.192.45.1` | `a1b2c3d4...` |
233| C | `/dev/ttyACM0` | `TollGate-4A2510` | `10.74.63.1` | `71bf3f4d...` |
234
235Ports change on every USB replug. Verify with `esptool.py --port <port> chip_id`.
236
237---
238
239## Checklist
240
241### Infrastructure
242
243- [x] Create `lib/esp32_board.py` — ESP32Board class (standalone, for programmatic use)
244- [x] Merge into existing `esp32/` test suite (conftest.py, pytest.ini, boards.py)
245- [x] Update `esp32/boards.env` with correct port assignments
246- [x] Add `relay` and `dns` helpers to `esp32/conftest.py`
247- [x] Add nsec env vars to `esp32/boards.env` (for CVM tests)
248- [x] Add Makefile targets for new test files
249
250### Test Files in `esp32/tests/`
251
252**Existing (pre-migration):**
253- [x] `test_smoke.py` — 6 tests, SSID visible, portal, grant, internet, reset
254- [x] `test_api.py` — 9 tests, debug, discovery, mints, wallet, whoami, usage, captcha URIs
255- [x] `test_captive_portal.py` — 7 tests, branding, price, input, button, placeholders, mint URL
256- [x] `test_multi_mint.py` — 4 tests, pricing, mint list, health, wallet
257- [x] `test_wallet_funding.py` — 2 tests, fund from upstream, spend (spend skipped)
258
259**New (migrated from .mjs):**
260- [x] `test_local_relay.py` — 5 tests, WS connect, REQ/EOSE, publish, close, multi-conn
261- [x] `test_relay_nip11.py` — 4 tests, JSON, fields, NIP support, name
262- [x] `test_market.py` — 3 tests, JSON, entry structure, empty state
263- [x] `test_cvm.py` — 4 tests, API reachable, 11316/11317 announcements, MCP roundtrip
264- [x] `test_reset_auth.py` — 6 tests, reset, payment, session lifecycle
265- [x] `test_session_expiry.py` — 1 test, 65s wait for expiry + renewal
266- [x] `test_dns_firewall.py` — 10 tests, DNS hijack, NAT filter, before/after auth/revoke
267
268### Verification
269
270- [x] `pytest --collect-only` — 60 tests collected from 12 files
271- [x] **37 passed, 1 skipped** — `pytest test_smoke.py test_api.py test_captive_portal.py test_multi_mint.py test_market.py test_relay_nip11.py test_local_relay.py --board=a`
272- [x] Lock files created/cleaned: `board-a.lock` lifecycle verified
273- [x] Relay tests pass: WS connect, EOSE, publish, close, NIP-11
274- [x] Market tests pass: JSON, entry structure
275- [ ] Payment-dependent tests (reset_auth, session_expiry, dns_firewall AfterAuth/AfterRevoke) — need mint available
276- [ ] Cross-board tests — need Board C provisioned and reachable
277- [ ] CVM roundtrip — needs nsec env var set and board connected to internet
278
279### Known Issues
280
2811. **Test ordering required** — `test_smoke.py` must run first (it's self-contained). `test_api.py` calls `reset_authentication` in `test_usage_endpoint_no_session` which breaks portal access for subsequent tests. Solution: run smoke first.
2822. **Relay max connections** — ESP32 relay supports ~2 simultaneous WebSocket connections. `test_multiple_connections` tests with 2 connections.
2833. **Board crashes after destructive tests** — `reset_authentication` in DNS/firewall tests can crash the HTTP server after multiple rapid resets. Board needs physical reset between test groups.
2844. **Payment-dependent tests blocked** — Mint intermittently returns `sqlite3.OperationalError: unable to open database file`. Tests work when mint is healthy.