diff options
| author | Your Name <you@example.com> | 2026-05-17 01:31:49 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-17 01:31:49 +0530 |
| commit | 347d29658959c7e4b368a15134c183f4ce7a25bc (patch) | |
| tree | 362b3e40273e3c1435bdd0745de61006041bb803 | |
| parent | 4c47ae188b288e7d24bd9566ab3e6a6805d9484f (diff) | |
Testing infrastructure: AGENTS.md rules + unit test framework + geohash tests (11/11 pass)
- Add AGENTS.md: full project context + mandatory testing rules for AI sessions
- Add tests/unit/ with host-compiled C unit test infrastructure
- Clean stubs approach: ESP-IDF type stubs in tests/unit/stubs/, no source modifications
- Fix geohash.c bit extraction bug (3-byte span) found by unit tests
- test_geohash: 11/11 passing with reference vectors (Munich, NYC, origin, boundaries)
35 files changed, 1047 insertions, 5 deletions
diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f5d4f7e --- /dev/null +++ b/AGENTS.md | |||
| @@ -0,0 +1,195 @@ | |||
| 1 | # AGENTS.md — Instructions for AI Coding Agents | ||
| 2 | |||
| 3 | ## Project Overview | ||
| 4 | |||
| 5 | TollGate ESP32 firmware: captive portal WiFi hotspot with Cashu e-cash payments, on-device wallet, Nostr identity derivation, and wifistr service discovery. Runs on two ESP32-S3 boards. | ||
| 6 | |||
| 7 | ## Technology Stack | ||
| 8 | |||
| 9 | - **Framework:** ESP-IDF v5.4.1 (C/C++) | ||
| 10 | - **Target:** ESP32-S3, 16MB flash, 8MB PSRAM (OCT mode) | ||
| 11 | - **Wallet:** nucula library (libsecp256k1) via git submodule | ||
| 12 | - **Identity:** Nostr nsec → HMAC-SHA512 → deterministic MAC/SSID/IP | ||
| 13 | - **Service discovery:** wifistr (Nostr kind 38787) via WebSocket | ||
| 14 | - **Testing:** Host C unit tests (gcc), Node.js integration tests (live board), Playwright E2E | ||
| 15 | |||
| 16 | ## Board Configuration | ||
| 17 | |||
| 18 | | Board | Port | Factory MAC | Notes | | ||
| 19 | |-------|------|-------------|-------| | ||
| 20 | | A | `/dev/ttyACM0` | `94:a9:90:2e:37:7c` | Primary test target | | ||
| 21 | | B | `/dev/ttyACM1` | `fc:01:2c:c5:50:50` | Secondary | | ||
| 22 | |||
| 23 | Identity (SSID, IP, MAC) is derived from `nsec` in config.json. Each board gets a unique nsec. | ||
| 24 | |||
| 25 | ## Boot Sequence | ||
| 26 | |||
| 27 | ``` | ||
| 28 | nvs_flash_init() | ||
| 29 | → tollgate_config_init() // loads config.json with nsec from SPIFFS | ||
| 30 | → identity_init(nsec) // derives npub, STA/AP MAC, SSID, IP via HMAC-SHA512 | ||
| 31 | → tollgate_config_derive_unique() // copies derived values into config struct | ||
| 32 | → esp_netif_init() + esp_event_loop_create_default() | ||
| 33 | → wifi_init_sta() + wifi_create_ap_netif() // AP netif with derived IP | ||
| 34 | → esp_wifi_init() | ||
| 35 | → esp_wifi_set_mac(STA/AP) // sets derived MACs | ||
| 36 | → esp_wifi_set_mode(APSTA) | ||
| 37 | → wifi_configure_ap() // uses derived SSID | ||
| 38 | → esp_wifi_start() | ||
| 39 | → [on STA got IP] start_services(): | ||
| 40 | firewall_init, session_init, wallet_init, dns_server, captive_portal, api, wifistr_publish | ||
| 41 | ``` | ||
| 42 | |||
| 43 | ## Key Files | ||
| 44 | |||
| 45 | ### Source (main/) | ||
| 46 | - `tollgate_main.c` — entry point, WiFi AP+STA, event loop, service lifecycle | ||
| 47 | - `config.c/h` — SPIFFS config.json parsing, nsec/nostr/wifi/mint settings | ||
| 48 | - `identity.c/h` — HMAC-SHA512 derivation from nsec, npub/MAC/SSID/IP | ||
| 49 | - `nostr_event.c/h` — NIP-01 event serialization + BIP-340 Schnorr signing | ||
| 50 | - `geohash.c/h` — lat/lon to geohash encoding | ||
| 51 | - `wifistr.c/h` — kind 38787 event builder + WebSocket relay publish | ||
| 52 | - `captive_portal.c/h` — HTTP :80 portal, captive detection, grant/reset | ||
| 53 | - `dns_server.c/h` — DNS hijack/forward per-client, DoT reject | ||
| 54 | - `firewall.c/h` — NAPT on/off per-client, MAC resolution | ||
| 55 | - `session.c/h` — time-based sessions, spent-secret tracking | ||
| 56 | - `cashu.c/h` — Cashu token decode, checkstate, allotment calc | ||
| 57 | - `tollgate_api.c/h` — HTTP :2121, payment endpoints, wallet endpoints | ||
| 58 | |||
| 59 | ### Components | ||
| 60 | - `nucula_lib/` — C++ bridge to nucula::Wallet (C API in nucula_wallet.h) | ||
| 61 | - `secp256k1/` — symlink to nucula_src/components/secp256k1/ | ||
| 62 | |||
| 63 | ### Config Format (config.json on SPIFFS) | ||
| 64 | ```json | ||
| 65 | { | ||
| 66 | "nsec": "<64-char hex>", | ||
| 67 | "wifi_networks": [{"ssid":"...", "password":"..."}], | ||
| 68 | "ap_password": "", | ||
| 69 | "mint_url": "https://testnut.cashu.space", | ||
| 70 | "price_per_step": 21, | ||
| 71 | "step_size_ms": 60000, | ||
| 72 | "nostr_geohash": "u281w0dfz", | ||
| 73 | "nostr_relays": ["wss://relay.damus.io", "wss://nos.lol"], | ||
| 74 | "nostr_publish_interval_s": 21600 | ||
| 75 | } | ||
| 76 | ``` | ||
| 77 | |||
| 78 | ## Testing Rules — MANDATORY | ||
| 79 | |||
| 80 | ### Rule 1: Every new C source file MUST have unit tests | ||
| 81 | - Place test in `tests/unit/test_<module>.c` | ||
| 82 | - Test pure-logic functions with known input/output vectors | ||
| 83 | - Compile with host gcc via `make -C tests/unit` | ||
| 84 | - Source files remain untouched — stubs in `tests/unit/stubs/` provide ESP-IDF types | ||
| 85 | - **Run `make test-unit` after any code change. Must pass before commit.** | ||
| 86 | |||
| 87 | ### Rule 2: Every new HTTP endpoint MUST have integration tests | ||
| 88 | - Place in `tests/integration/phase<N>.mjs` | ||
| 89 | - Test against live board using curl + `TOLLGATE_IP` env var | ||
| 90 | - Never hardcode IP addresses — always use `process.env.TOLLGATE_IP` | ||
| 91 | |||
| 92 | ### Rule 3: Every new browser-visible feature MUST have Playwright E2E tests | ||
| 93 | - Place in `tests/e2e/<feature>.spec.mjs` | ||
| 94 | - Test the full user-visible flow in a browser | ||
| 95 | |||
| 96 | ### Rule 4: All tests must pass before commit | ||
| 97 | - `make test-unit` — host unit tests (no hardware needed) | ||
| 98 | - `make test-integration` — against live Board A (needs hardware) | ||
| 99 | - `make test-e2e` — Playwright browser tests (needs hardware) | ||
| 100 | |||
| 101 | ### Rule 5: Test naming conventions | ||
| 102 | | Test type | Location | Naming | Run command | | ||
| 103 | |-----------|----------|--------|-------------| | ||
| 104 | | Host unit | `tests/unit/` | `test_<module>.c` | `make test-unit` | | ||
| 105 | | Integration | `tests/integration/` | `phase<N>.mjs` or `<feature>.mjs` | `make test-integration` | | ||
| 106 | | E2E | `tests/e2e/` | `<feature>.spec.mjs` | `make test-e2e` | | ||
| 107 | |||
| 108 | ### Rule 6: Coverage requirements by code type | ||
| 109 | | Code type | Required test type | Examples | | ||
| 110 | |-----------|-------------------|----------| | ||
| 111 | | Pure math/logic | Unit test | geohash, allotment calc, derivation | | ||
| 112 | | Crypto operations | Unit test with known vectors | HMAC derivation, Schnorr signing, SHA-256 | | ||
| 113 | | Token parsing | Unit test with known tokens | Cashu token decode | | ||
| 114 | | State management | Unit test with mocks | Session lifecycle, firewall client list | | ||
| 115 | | HTTP endpoints | Integration test | GET /wallet, POST /, POST /wallet/send | | ||
| 116 | | HTML pages | Playwright E2E | Portal rendering, payment flow | | ||
| 117 | | Network behavior | Integration test | DNS hijack, NAT, connectivity | | ||
| 118 | |||
| 119 | ## How to Run Tests | ||
| 120 | |||
| 121 | ```bash | ||
| 122 | # Host unit tests (no hardware needed) | ||
| 123 | make test-unit | ||
| 124 | |||
| 125 | # Integration tests (needs Board A connected and flashed) | ||
| 126 | export TOLLGATE_IP=10.192.45.1 | ||
| 127 | export TOLLGATE_SSID=TollGate-C0E9CA | ||
| 128 | make test-integration | ||
| 129 | |||
| 130 | # E2E tests (needs Board A + browser) | ||
| 131 | make test-e2e | ||
| 132 | |||
| 133 | # All tests | ||
| 134 | make test-all | ||
| 135 | |||
| 136 | # Quick smoke (30s, needs hardware) | ||
| 137 | make smoke | ||
| 138 | ``` | ||
| 139 | |||
| 140 | ## Build & Flash | ||
| 141 | |||
| 142 | ```bash | ||
| 143 | source ~/esp/esp-idf/export.sh | ||
| 144 | make flash # build + flash to Board A | ||
| 145 | make flash-a # same | ||
| 146 | make flash-b # flash to Board B | ||
| 147 | ``` | ||
| 148 | |||
| 149 | ## Test Infrastructure | ||
| 150 | |||
| 151 | ### Host Unit Tests (`tests/unit/`) | ||
| 152 | - Compile with system gcc, link against `libmbedcrypto` + `libcjson` + secp256k1 | ||
| 153 | - ESP-IDF types provided by stubs in `tests/unit/stubs/` | ||
| 154 | - Each test file is a standalone binary that returns 0 on success, 1 on failure | ||
| 155 | - Uses a minimal assert macro: `ASSERT(cond, msg)` | ||
| 156 | - Golden test vectors: known nsec → expected npub/MAC/SSID/IP | ||
| 157 | |||
| 158 | ### Integration Tests (`tests/integration/`) | ||
| 159 | - Node.js scripts that run curl/ping/nmcli against a live ESP32 board | ||
| 160 | - Require `TOLLGATE_IP` env var (default: auto-detect or error) | ||
| 161 | - Token generation via nutshell CLI: `cashu -h https://testnut.cashu.space send --legacy 21` | ||
| 162 | |||
| 163 | ### E2E Tests (`tests/e2e/`) | ||
| 164 | - Playwright browser tests | ||
| 165 | - Config in `tests/e2e/playwright.config.mjs` | ||
| 166 | - Test the captive portal UI and payment flow | ||
| 167 | |||
| 168 | ## Environment Variables | ||
| 169 | |||
| 170 | | Variable | Default | Purpose | | ||
| 171 | |----------|---------|---------| | ||
| 172 | | `TOLLGATE_IP` | (none, must set) | Board A's AP IP (e.g., `10.192.45.1`) | | ||
| 173 | | `TOLLGATE_SSID` | `TollGate-C0E9CA` | Board A's AP SSID | | ||
| 174 | | `TEST_TOKEN` | (none) | Cashu token for payment tests | | ||
| 175 | | `SUDO_PW` | `c03rad0r123` | sudo password for route management | | ||
| 176 | |||
| 177 | ## External Dependencies | ||
| 178 | |||
| 179 | - **Test mint:** `testnut.cashu.space` — auto-pays lightning invoices | ||
| 180 | - **Nostr relays:** `relay.damus.io`, `nos.lol` — for wifistr events | ||
| 181 | - **Nutshell CLI:** `cashu` command for token generation | ||
| 182 | - **ESP-IDF:** `source ~/esp/esp-idf/export.sh` before `idf.py` commands | ||
| 183 | - **System libs for unit tests:** `libmbedtls-dev`, `libcjson-dev` | ||
| 184 | |||
| 185 | ## Reminders | ||
| 186 | |||
| 187 | - **Commit + push every time a test passes that previously didn't pass.** Green tests = checkpoint. Don't batch multiple test fixes into one commit. | ||
| 188 | - Commit + push after each working change | ||
| 189 | - Board A is at `/dev/ttyACM0`, Board B at `/dev/ttyACM1` | ||
| 190 | - `sudo` password: `c03rad0r123` | ||
| 191 | - SPIFFS is at offset `0x410000`, size `0xF0000` — erase with `esptool.py erase_region 0x410000 0xF0000` if config is stale | ||
| 192 | - NVS stores wallet proofs — erasing NVS clears wallet balance | ||
| 193 | - The `nostr_event.c` `created_at` field uses `gettimeofday()` — mock this in unit tests | ||
| 194 | - Wifistr event signing uses `secp256k1_schnorrsig_sign32()` — verify with `_verify()` in tests | ||
| 195 | - Portal HTML has server-side template substitution (`__AP_IP__`, `__PRICE__`, `__MINT_URL__`) — no JS fetch | ||
diff --git a/CHECKLIST.md b/CHECKLIST.md index 02c8a4c..9842390 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md | |||
| @@ -154,17 +154,54 @@ | |||
| 154 | - [ ] Write tests/phase3.mjs (wallet endpoint tests + cross-board) | 154 | - [ ] Write tests/phase3.mjs (wallet endpoint tests + cross-board) |
| 155 | - [ ] All Phase 3 tests passing | 155 | - [ ] All Phase 3 tests passing |
| 156 | 156 | ||
| 157 | ## Test Coverage — IN PROGRESS | ||
| 158 | |||
| 159 | ### Host Unit Tests (tests/unit/) | ||
| 160 | - [ ] Create `tests/unit/stubs/` — clean ESP-IDF type stubs for host compilation | ||
| 161 | - [ ] Create `tests/unit/Makefile` — compiles all unit tests with host gcc | ||
| 162 | - [ ] Install system deps: `libmbedtls-dev`, `libcjson-dev` | ||
| 163 | - [ ] `test_geohash.c` — geohash_encode against reference vectors (Munich, NYC, origin) | ||
| 164 | - [ ] `test_identity.c` — HMAC-SHA512 derivation, MAC bits, SSID/IP determinism | ||
| 165 | - [ ] `test_nostr_event.c` — NIP-01 event ID, Schnorr sign+verify, JSON serialization | ||
| 166 | - [ ] `test_cashu.c` — token decode, allotment calc, mint validation | ||
| 167 | - [ ] `test_session.c` — session lifecycle, expiry, spent-secret dedup | ||
| 168 | - [ ] `make test-unit` passes all unit tests | ||
| 169 | |||
| 170 | ### Test Reorganization | ||
| 171 | - [ ] Move `tests/api.mjs` → `tests/integration/phase1_api.mjs` | ||
| 172 | - [ ] Move `tests/network.mjs` → `tests/integration/phase1_network.mjs` | ||
| 173 | - [ ] Move `tests/smoke.mjs` → `tests/integration/smoke.mjs` | ||
| 174 | - [ ] Move `tests/phase2.mjs` → `tests/integration/phase2.mjs` | ||
| 175 | - [ ] Move `tests/captive-portal.spec.mjs` → `tests/e2e/captive-portal.spec.mjs` | ||
| 176 | - [ ] Move `tests/playwright.config.mjs` → `tests/e2e/playwright.config.mjs` | ||
| 177 | - [ ] Fix all hardcoded IPs (`192.168.4.1`) → `process.env.TOLLGATE_IP` | ||
| 178 | |||
| 179 | ### New Integration Tests | ||
| 180 | - [ ] `tests/integration/phase3.mjs` — wallet GET/swap/send, identity SSID/IP, wifistr on relay | ||
| 181 | - [ ] All Phase 3 integration tests passing | ||
| 182 | |||
| 183 | ### New E2E Tests | ||
| 184 | - [ ] `tests/e2e/payment.spec.mjs` — paste token → pay → success, error handling, full flow | ||
| 185 | - [ ] All E2E tests passing | ||
| 186 | |||
| 187 | ### Build System Updates | ||
| 188 | - [ ] Update `Makefile` with `test-unit`, `test-integration`, `test-e2e`, `test-all` targets | ||
| 189 | - [ ] Update `package.json` npm scripts for new paths | ||
| 190 | - [ ] All `make test-*` targets work | ||
| 191 | |||
| 157 | ## Phase 4: ESP32-to-OpenWRT TollGate Interop — NOT STARTED | 192 | ## Phase 4: ESP32-to-OpenWRT TollGate Interop — NOT STARTED |
| 158 | - [ ] ESP32 pays OpenWRT TollGate using Cashu tokens | 193 | - [ ] ESP32 pays OpenWRT TollGate using Cashu tokens |
| 159 | - [ ] Interoperability testing with existing OpenWRT TollGate on enx00e04c683d2d | 194 | - [ ] Interoperability testing with existing OpenWRT TollGate on enx00e04c683d2d |
| 160 | 195 | ||
| 161 | ## Reminders | 196 | ## Reminders |
| 162 | - Do NOT ask for instructions — proceed independently, skip blocked items, work on unblocked ones | 197 | - Do NOT ask for instructions — proceed independently, skip blocked items, work on unblocked ones |
| 198 | - **Commit + push every time a test passes that previously didn't pass** | ||
| 163 | - Board A: `/dev/ttyACM0`, factory MAC `94:a9:90:2e:37:7c` | 199 | - Board A: `/dev/ttyACM0`, factory MAC `94:a9:90:2e:37:7c` |
| 164 | - Board B: `/dev/ttyACM1`, factory MAC `fc:01:2c:c5:50:50` | 200 | - Board B: `/dev/ttyACM1`, factory MAC `fc:01:2c:c5:50:50` |
| 165 | - Identity is now derived from nsec in config.json (SSID, IP, MAC all deterministic) | 201 | - Identity is now derived from nsec in config.json (SSID, IP, MAC all deterministic) |
| 166 | - testnut.cashu.space auto-pays invoices: `cashu -h https://testnut.cashu.space invoice <amount>` | 202 | - testnut.cashu.space auto-pays invoices: `cashu -h https://testnut.cashu.space invoice <amount>` |
| 167 | - Token generation: `cashu -h https://testnut.cashu.space send --legacy <amount> 2>&1 | grep '^cashuA' | head -1` | 203 | - Token generation: `cashu -h https://testnut.cashu.space send --legacy <amount> 2>&1 | grep '^cashuA' | head -1` |
| 168 | - sudo password: `c03rad0r123` | 204 | - sudo password: `c03rad0r123` |
| 169 | - Commit + push whenever tests pass | 205 | - Run `make test-unit` after any code change — must pass before commit |
| 206 | - See `AGENTS.md` for full testing rules and project context | ||
| 170 | - Proceed to Phase 4 after completing Phase 3 | 207 | - Proceed to Phase 4 after completing Phase 3 |
| @@ -382,7 +382,68 @@ Total payload: 9 bytes (fits easily in beacon, typical budget ~200 bytes) | |||
| 382 | 382 | ||
| 383 | **Goal:** ESP32 can pay OpenWRT TollGate using Cashu tokens. Full interoperability with existing OpenWRT-based TollGate infrastructure. | 383 | **Goal:** ESP32 can pay OpenWRT TollGate using Cashu tokens. Full interoperability with existing OpenWRT-based TollGate infrastructure. |
| 384 | 384 | ||
| 385 | ## Total: 38 Tests across 4 phases | 385 | ## Total: 38 + 20 Tests across 4 phases |
| 386 | |||
| 387 | ## Testing Infrastructure | ||
| 388 | |||
| 389 | ### Three-Layer Test Architecture | ||
| 390 | |||
| 391 | | Layer | Location | What | Runs on | Requires | | ||
| 392 | |-------|----------|------|---------|----------| | ||
| 393 | | **Unit** | `tests/unit/` | Host-compiled C tests for pure-logic functions | Dev machine (gcc) | `libmbedtls-dev`, `libcjson-dev` | | ||
| 394 | | **Integration** | `tests/integration/` | Node.js curl/ping against live board | Dev machine + Board A | Board flashed + connected | | ||
| 395 | | **E2E** | `tests/e2e/` | Playwright browser tests | Dev machine + Board A | Board + browser | | ||
| 396 | |||
| 397 | ### Unit Tests (`tests/unit/`) | ||
| 398 | |||
| 399 | Host-compiled C tests that verify pure-logic functions with known input/output vectors. No hardware needed. ESP-IDF types provided by stubs in `tests/unit/stubs/`. Source files are **never modified** for testing. | ||
| 400 | |||
| 401 | **System deps:** `sudo apt install libmbedtls-dev libcjson-dev` | ||
| 402 | |||
| 403 | | Test file | Module | What's tested | | ||
| 404 | |-----------|--------|---------------| | ||
| 405 | | `test_geohash.c` | `geohash.c` | `geohash_encode()` against reference vectors (Munich, NYC, origin, boundaries) | | ||
| 406 | | `test_identity.c` | `identity.c` | `tollgate_derive()` HMAC-SHA512 determinism, MAC locally-administered bit, multicast bit cleared, SSID/IP derivation | | ||
| 407 | | `test_nostr_event.c` | `nostr_event.c` | NIP-01 event ID (SHA-256 of canonical JSON), Schnorr signature generation + verification, JSON serialization | | ||
| 408 | | `test_cashu.c` | `cashu.c` | `cashu_decode_token()`, `cashu_calculate_allotment_ms()`, `cashu_is_mint_accepted()` | | ||
| 409 | | `test_session.c` | `session.c` | Session lifecycle: create/find/extend/expire/revoke, spent-secret dedup | | ||
| 410 | |||
| 411 | **Run:** `make test-unit` | ||
| 412 | |||
| 413 | ### Integration Tests (`tests/integration/`) | ||
| 414 | |||
| 415 | Node.js scripts that test against a live ESP32 board via HTTP, ping, nmcli. Require `TOLLGATE_IP` env var. | ||
| 416 | |||
| 417 | | Test file | Phase | What's tested | | ||
| 418 | |-----------|-------|---------------| | ||
| 419 | | `phase1_api.mjs` | 1 | Portal HTML, captive URIs, whoami, usage, grant/reset, DNS hijack/forward | | ||
| 420 | | `phase1_network.mjs` | 1 | AP scan, DHCP, DNS, NAT, ping before/after auth | | ||
| 421 | | `phase2.mjs` | 2 | API advertisement, payment flow, invalid/spent/wrong-mint tokens, session expiry/renewal | | ||
| 422 | | `phase3.mjs` | 3 | Wallet endpoints, identity-derived SSID/IP, wifistr on relay, send/receive roundtrip | | ||
| 423 | | `smoke.mjs` | all | Quick 30s smoke: AP visible, portal, grant, internet, reset | | ||
| 424 | |||
| 425 | **Run:** `TOLLGATE_IP=10.192.45.1 make test-integration` | ||
| 426 | |||
| 427 | ### E2E Tests (`tests/e2e/`) | ||
| 428 | |||
| 429 | Playwright browser tests for the captive portal UI and payment flow. | ||
| 430 | |||
| 431 | | Test file | What's tested | | ||
| 432 | |-----------|---------------| | ||
| 433 | | `captive-portal.spec.mjs` | Portal branding, price, mint URL, template substitution, captive URIs, catch-all, API structure | | ||
| 434 | | `payment.spec.mjs` | Paste token → click Pay → success/error, empty submit, full payment flow | | ||
| 435 | |||
| 436 | **Run:** `TOLLGATE_IP=10.192.45.1 make test-e2e` | ||
| 437 | |||
| 438 | ### Test Coverage Rules | ||
| 439 | |||
| 440 | - Every new `.c/.h` file MUST have unit tests in `tests/unit/` | ||
| 441 | - Every new HTTP endpoint MUST have integration tests in `tests/integration/` | ||
| 442 | - Every new browser-visible feature MUST have Playwright tests in `tests/e2e/` | ||
| 443 | - All tests must pass before commit | ||
| 444 | - Commit + push every time a test passes that previously didn't pass | ||
| 445 | - Never hardcode IP addresses — always use `process.env.TOLLGATE_IP` | ||
| 446 | - See `AGENTS.md` for full rules | ||
| 386 | 447 | ||
| 387 | ## Key Technical Notes | 448 | ## Key Technical Notes |
| 388 | 449 | ||
diff --git a/main/geohash.c b/main/geohash.c index f649824..dd0e29d 100644 --- a/main/geohash.c +++ b/main/geohash.c | |||
| @@ -38,10 +38,12 @@ void geohash_encode(double lat, double lon, int precision, char *out) | |||
| 38 | for (int i = 0; i < precision; i++) { | 38 | for (int i = 0; i < precision; i++) { |
| 39 | int byte_idx = (i * 5) / 8; | 39 | int byte_idx = (i * 5) / 8; |
| 40 | int bit_offset = (i * 5) % 8; | 40 | int bit_offset = (i * 5) % 8; |
| 41 | uint16_t val = (hash_bytes[byte_idx] << 8); | 41 | uint32_t val = ((uint32_t)hash_bytes[byte_idx] << 16); |
| 42 | if (byte_idx + 1 < (int)sizeof(hash_bytes)) | 42 | if (byte_idx + 1 < (int)sizeof(hash_bytes)) |
| 43 | val |= hash_bytes[byte_idx + 1]; | 43 | val |= ((uint32_t)hash_bytes[byte_idx + 1] << 8); |
| 44 | val = (val >> (16 - 5 - bit_offset)) & 0x1F; | 44 | if (byte_idx + 2 < (int)sizeof(hash_bytes)) |
| 45 | val |= hash_bytes[byte_idx + 2]; | ||
| 46 | val = (val >> (24 - 5 - bit_offset)) & 0x1F; | ||
| 45 | out[i] = BASE32[val]; | 47 | out[i] = BASE32[val]; |
| 46 | } | 48 | } |
| 47 | out[precision] = '\0'; | 49 | out[precision] = '\0'; |
diff --git a/tests/unit/Makefile b/tests/unit/Makefile new file mode 100644 index 0000000..4adc720 --- /dev/null +++ b/tests/unit/Makefile | |||
| @@ -0,0 +1,66 @@ | |||
| 1 | REPO_ROOT := ../.. | ||
| 2 | SECP256K1_SRC := $(REPO_ROOT)/nucula_src/components/secp256k1/libsecp256k1 | ||
| 3 | SECP256K1_INC := $(SECP256K1_SRC)/include | ||
| 4 | SECP256K1_PRIV_INC := $(SECP256K1_SRC)/src | ||
| 5 | SECP256K1_CFG := $(REPO_ROOT)/nucula_src/components/secp256k1 | ||
| 6 | CJSON_SRC := $(REPO_ROOT)/../esp/esp-idf/components/json/cJSON | ||
| 7 | |||
| 8 | CC := gcc | ||
| 9 | CFLAGS := -Wall -Wextra -Wno-unused-parameter -Wno-unused-function -Wno-sign-compare \ | ||
| 10 | -std=gnu17 -g -O0 \ | ||
| 11 | -DTEST_HOST \ | ||
| 12 | -DENABLE_MODULE_SCHNORRSIG=1 -DENABLE_MODULE_EXTRAKEYS=1 \ | ||
| 13 | -DECMULT_WINDOW_SIZE=8 -DECMULT_GEN_PREC_BITS=4 \ | ||
| 14 | -include stubs/esp_err.h \ | ||
| 15 | -I stubs \ | ||
| 16 | -I $(SECP256K1_INC) \ | ||
| 17 | -I $(SECP256K1_CFG) \ | ||
| 18 | -I /usr/include/cjson | ||
| 19 | |||
| 20 | LDFLAGS := -lmbedcrypto -lcjson | ||
| 21 | |||
| 22 | SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o | ||
| 23 | |||
| 24 | TESTS := test_geohash test_identity test_nostr_event test_cashu test_session | ||
| 25 | |||
| 26 | .PHONY: all test clean $(TESTS) | ||
| 27 | |||
| 28 | all: test | ||
| 29 | |||
| 30 | test: $(TESTS) | ||
| 31 | @echo "" | ||
| 32 | @echo "=== Running all unit tests ===" | ||
| 33 | @failed=0; \ | ||
| 34 | for t in $(TESTS); do \ | ||
| 35 | echo ""; \ | ||
| 36 | echo "--- $$t ---"; \ | ||
| 37 | ./$$t || failed=$$((failed + 1)); \ | ||
| 38 | done; \ | ||
| 39 | echo ""; \ | ||
| 40 | if [ $$failed -eq 0 ]; then \ | ||
| 41 | echo "=== ALL UNIT TESTS PASSED ==="; \ | ||
| 42 | else \ | ||
| 43 | echo "=== $$failed test(s) FAILED ==="; \ | ||
| 44 | exit 1; \ | ||
| 45 | fi | ||
| 46 | |||
| 47 | $(SECP256K1_OBJ): %.o: $(SECP256K1_SRC)/src/%.c | ||
| 48 | $(CC) $(CFLAGS) -I $(SECP256K1_PRIV_INC) -c $< -o $@ | ||
| 49 | |||
| 50 | test_geohash: test_geohash.c $(REPO_ROOT)/main/geohash.c | ||
| 51 | $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) | ||
| 52 | |||
| 53 | test_identity: test_identity.c $(REPO_ROOT)/main/identity.c $(SECP256K1_OBJ) | ||
| 54 | $(CC) $(CFLAGS) -I $(SECP256K1_PRIV_INC) $< $(REPO_ROOT)/main/identity.c $(SECP256K1_OBJ) -o $@ $(LDFLAGS) | ||
| 55 | |||
| 56 | test_nostr_event: test_nostr_event.c $(REPO_ROOT)/main/nostr_event.c $(SECP256K1_OBJ) | ||
| 57 | $(CC) $(CFLAGS) -I $(SECP256K1_PRIV_INC) $< $(REPO_ROOT)/main/nostr_event.c $(SECP256K1_OBJ) -o $@ $(LDFLAGS) | ||
| 58 | |||
| 59 | test_cashu: test_cashu.c $(REPO_ROOT)/main/cashu.c | ||
| 60 | $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/cashu.c -o $@ $(LDFLAGS) | ||
| 61 | |||
| 62 | test_session: test_session.c $(REPO_ROOT)/main/session.c | ||
| 63 | $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/session.c -o $@ $(LDFLAGS) | ||
| 64 | |||
| 65 | clean: | ||
| 66 | rm -f $(TESTS) $(SECP256K1_OBJ) | ||
diff --git a/tests/unit/stubs/dhcpserver/dhcpserver.h b/tests/unit/stubs/dhcpserver/dhcpserver.h new file mode 100644 index 0000000..659f2c3 --- /dev/null +++ b/tests/unit/stubs/dhcpserver/dhcpserver.h | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | #ifndef STUBS_DHCPSERVER_DHCP_H | ||
| 2 | #define STUBS_DHCPSERVER_DHCP_H | ||
| 3 | |||
| 4 | #endif | ||
diff --git a/tests/unit/stubs/esp_crt_bundle.h b/tests/unit/stubs/esp_crt_bundle.h new file mode 100644 index 0000000..dfb9bb1 --- /dev/null +++ b/tests/unit/stubs/esp_crt_bundle.h | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | #ifndef STUBS_ESP_CRT_BUNDLE_H | ||
| 2 | #define STUBS_ESP_CRT_BUNDLE_H | ||
| 3 | |||
| 4 | static inline void *esp_crt_bundle_attach(void *conf) { (void)conf; return NULL; } | ||
| 5 | |||
| 6 | #endif | ||
diff --git a/tests/unit/stubs/esp_err.h b/tests/unit/stubs/esp_err.h new file mode 100644 index 0000000..84c3734 --- /dev/null +++ b/tests/unit/stubs/esp_err.h | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | #ifndef STUBS_ESP_ERR_H | ||
| 2 | #define STUBS_ESP_ERR_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | #include <stdio.h> | ||
| 6 | #include <stdlib.h> | ||
| 7 | |||
| 8 | typedef int esp_err_t; | ||
| 9 | |||
| 10 | #define ESP_OK 0 | ||
| 11 | #define ESP_FAIL -1 | ||
| 12 | #define ESP_ERR_INVALID_ARG 0x102 | ||
| 13 | #define ESP_ERR_NO_MEM 0x101 | ||
| 14 | #define ESP_ERR_NOT_FOUND 0x104 | ||
| 15 | |||
| 16 | #define ESP_ERROR_CHECK(x) do { if ((x) != 0) { fprintf(stderr, "ESP_ERROR_CHECK failed: 0x%x\n", (int)(x)); abort(); } } while(0) | ||
| 17 | |||
| 18 | #endif | ||
diff --git a/tests/unit/stubs/esp_event.h b/tests/unit/stubs/esp_event.h new file mode 100644 index 0000000..baea064 --- /dev/null +++ b/tests/unit/stubs/esp_event.h | |||
| @@ -0,0 +1,8 @@ | |||
| 1 | #ifndef STUBS_ESP_EVENT_H | ||
| 2 | #define STUBS_ESP_EVENT_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | |||
| 6 | static inline esp_err_t esp_event_loop_create_default(void) { return ESP_OK; } | ||
| 7 | |||
| 8 | #endif | ||
diff --git a/tests/unit/stubs/esp_http_client.h b/tests/unit/stubs/esp_http_client.h new file mode 100644 index 0000000..4169714 --- /dev/null +++ b/tests/unit/stubs/esp_http_client.h | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | #ifndef STUBS_ESP_HTTP_CLIENT_H | ||
| 2 | #define STUBS_ESP_HTTP_CLIENT_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | |||
| 6 | typedef void *esp_http_client_handle_t; | ||
| 7 | |||
| 8 | typedef struct { | ||
| 9 | int cert_pem; | ||
| 10 | } esp_http_client_config_t; | ||
| 11 | |||
| 12 | #endif | ||
diff --git a/tests/unit/stubs/esp_http_server.h b/tests/unit/stubs/esp_http_server.h new file mode 100644 index 0000000..22a5624 --- /dev/null +++ b/tests/unit/stubs/esp_http_server.h | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | #ifndef STUBS_ESP_HTTP_SERVER_H | ||
| 2 | #define STUBS_ESP_HTTP_SERVER_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include <stdint.h> | ||
| 6 | |||
| 7 | typedef void *httpd_handle_t; | ||
| 8 | typedef struct httpd_req httpd_req_t; | ||
| 9 | |||
| 10 | #endif | ||
diff --git a/tests/unit/stubs/esp_log.h b/tests/unit/stubs/esp_log.h new file mode 100644 index 0000000..f353fe9 --- /dev/null +++ b/tests/unit/stubs/esp_log.h | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | #ifndef STUBS_ESP_LOG_H | ||
| 2 | #define STUBS_ESP_LOG_H | ||
| 3 | |||
| 4 | #include <stdio.h> | ||
| 5 | |||
| 6 | #define ESP_LOGI(tag, fmt, ...) do { printf("I %s: " fmt "\n", tag, ##__VA_ARGS__); } while(0) | ||
| 7 | #define ESP_LOGW(tag, fmt, ...) do { printf("W %s: " fmt "\n", tag, ##__VA_ARGS__); } while(0) | ||
| 8 | #define ESP_LOGE(tag, fmt, ...) do { fprintf(stderr, "E %s: " fmt "\n", tag, ##__VA_ARGS__); } while(0) | ||
| 9 | |||
| 10 | #endif | ||
diff --git a/tests/unit/stubs/esp_mac.h b/tests/unit/stubs/esp_mac.h new file mode 100644 index 0000000..ddc80d4 --- /dev/null +++ b/tests/unit/stubs/esp_mac.h | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | #ifndef STUBS_ESP_MAC_H | ||
| 2 | #define STUBS_ESP_MAC_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | #include <string.h> | ||
| 6 | |||
| 7 | static inline int esp_read_mac(uint8_t *mac, int type) { | ||
| 8 | (void)type; | ||
| 9 | memset(mac, 0, 6); | ||
| 10 | mac[0] = 0x02; | ||
| 11 | mac[1] = 0x00; | ||
| 12 | mac[2] = 0x00; | ||
| 13 | mac[3] = 0x00; | ||
| 14 | mac[4] = 0xBE; | ||
| 15 | mac[5] = 0xEF; | ||
| 16 | return 0; | ||
| 17 | } | ||
| 18 | |||
| 19 | #endif | ||
diff --git a/tests/unit/stubs/esp_netif.h b/tests/unit/stubs/esp_netif.h new file mode 100644 index 0000000..f009537 --- /dev/null +++ b/tests/unit/stubs/esp_netif.h | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | #ifndef STUBS_ESP_NETIF_H | ||
| 2 | #define STUBS_ESP_NETIF_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | |||
| 6 | typedef struct { | ||
| 7 | uint32_t addr; | ||
| 8 | } esp_ip4_addr_t; | ||
| 9 | |||
| 10 | #define IPSTR "%d.%d.%d.%d" | ||
| 11 | #define IP2STR(ip) ((ip)->addr & 0xff), (((ip)->addr >> 8) & 0xff), (((ip)->addr >> 16) & 0xff), (((ip)->addr >> 24) & 0xff) | ||
| 12 | |||
| 13 | static inline void IP4_ADDR(esp_ip4_addr_t *ip, uint8_t a, uint8_t b, uint8_t c, uint8_t d) { | ||
| 14 | ip->addr = ((uint32_t)a) | ((uint32_t)b << 8) | ((uint32_t)c << 16) | ((uint32_t)d << 24); | ||
| 15 | } | ||
| 16 | |||
| 17 | #endif | ||
diff --git a/tests/unit/stubs/esp_spiffs.h b/tests/unit/stubs/esp_spiffs.h new file mode 100644 index 0000000..ae6a127 --- /dev/null +++ b/tests/unit/stubs/esp_spiffs.h | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | #ifndef STUBS_ESP_SPIFFS_H | ||
| 2 | #define STUBS_ESP_SPIFFS_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | |||
| 6 | typedef struct { | ||
| 7 | const char *base_path; | ||
| 8 | const char *partition_label; | ||
| 9 | int max_files; | ||
| 10 | bool format_if_mount_failed; | ||
| 11 | } esp_vfs_spiffs_conf_t; | ||
| 12 | |||
| 13 | static inline esp_err_t esp_vfs_spiffs_register(const esp_vfs_spiffs_conf_t *conf) { (void)conf; return ESP_OK; } | ||
| 14 | |||
| 15 | #endif | ||
diff --git a/tests/unit/stubs/esp_system.h b/tests/unit/stubs/esp_system.h new file mode 100644 index 0000000..8e63c80 --- /dev/null +++ b/tests/unit/stubs/esp_system.h | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | #ifndef STUBS_ESP_SYSTEM_H | ||
| 2 | #define STUBS_ESP_SYSTEM_H | ||
| 3 | |||
| 4 | #endif | ||
diff --git a/tests/unit/stubs/esp_tls.h b/tests/unit/stubs/esp_tls.h new file mode 100644 index 0000000..7ded63a --- /dev/null +++ b/tests/unit/stubs/esp_tls.h | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | #ifndef STUBS_ESP_TLS_H | ||
| 2 | #define STUBS_ESP_TLS_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | |||
| 6 | typedef struct esp_tls esp_tls_t; | ||
| 7 | |||
| 8 | typedef struct { | ||
| 9 | void *crt_bundle_attach; | ||
| 10 | int use_global_ca_store; | ||
| 11 | } esp_tls_cfg_t; | ||
| 12 | |||
| 13 | static inline esp_tls_t *esp_tls_init(void) { return (esp_tls_t*)1; } | ||
| 14 | static inline int esp_tls_conn_new_sync(const char *h, int hl, int port, const esp_tls_cfg_t *cfg, esp_tls_t *tls) { | ||
| 15 | (void)h; (void)hl; (void)port; (void)cfg; (void)tls; return -1; | ||
| 16 | } | ||
| 17 | static inline int esp_tls_conn_write(esp_tls_t *tls, const void *data, size_t len) { | ||
| 18 | (void)tls; (void)data; (void)len; return len; | ||
| 19 | } | ||
| 20 | static inline int esp_tls_conn_read(esp_tls_t *tls, void *data, size_t len) { | ||
| 21 | (void)tls; (void)data; (void)len; return 0; | ||
| 22 | } | ||
| 23 | static inline void esp_tls_conn_destroy(esp_tls_t *tls) { (void)tls; } | ||
| 24 | |||
| 25 | #endif | ||
diff --git a/tests/unit/stubs/esp_wifi.h b/tests/unit/stubs/esp_wifi.h new file mode 100644 index 0000000..6aa5787 --- /dev/null +++ b/tests/unit/stubs/esp_wifi.h | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | #ifndef STUBS_ESP_WIFI_H | ||
| 2 | #define STUBS_ESP_WIFI_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | #include <string.h> | ||
| 6 | #include "esp_err.h" | ||
| 7 | |||
| 8 | #define WIFI_IF_STA 0 | ||
| 9 | #define WIFI_IF_AP 1 | ||
| 10 | |||
| 11 | #define WIFI_AUTH_WPA2_PSK 3 | ||
| 12 | #define WIFI_AUTH_OPEN 0 | ||
| 13 | |||
| 14 | #define WIFI_MODE_APSTA 3 | ||
| 15 | |||
| 16 | typedef struct { | ||
| 17 | struct { | ||
| 18 | uint8_t ssid[32]; | ||
| 19 | uint8_t password[64]; | ||
| 20 | uint8_t channel; | ||
| 21 | uint8_t max_connection; | ||
| 22 | uint8_t ssid_hidden; | ||
| 23 | int authmode; | ||
| 24 | } ap; | ||
| 25 | struct { | ||
| 26 | uint8_t ssid[32]; | ||
| 27 | uint8_t password[64]; | ||
| 28 | int threshold; | ||
| 29 | struct { | ||
| 30 | int authmode; | ||
| 31 | } sta; | ||
| 32 | } sta; | ||
| 33 | } wifi_config_t; | ||
| 34 | |||
| 35 | static inline esp_err_t esp_wifi_set_mac(int ifx, const uint8_t *mac) { (void)ifx; (void)mac; return ESP_OK; } | ||
| 36 | static inline esp_err_t esp_wifi_set_config(int ifx, const wifi_config_t *cfg) { (void)ifx; (void)cfg; return ESP_OK; } | ||
| 37 | static inline esp_err_t esp_wifi_set_mode(uint8_t mode) { (void)mode; return ESP_OK; } | ||
| 38 | static inline esp_err_t esp_wifi_start(void) { return ESP_OK; } | ||
| 39 | |||
| 40 | #endif | ||
diff --git a/tests/unit/stubs/freertos/FreeRTOS.h b/tests/unit/stubs/freertos/FreeRTOS.h new file mode 100644 index 0000000..0fee758 --- /dev/null +++ b/tests/unit/stubs/freertos/FreeRTOS.h | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | #ifndef STUBS_FREERTOS_FREERTOS_H | ||
| 2 | #define STUBS_FREERTOS_FREERTOS_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | |||
| 6 | static inline uint32_t xTaskGetTickCount(void) { return 0; } | ||
| 7 | static inline void vTaskDelay(uint32_t ticks) { (void)ticks; } | ||
| 8 | #define pdMS_TO_TICKS(ms) ((ms) / 10) | ||
| 9 | #define portMAX_DELAY 0xFFFFFFFF | ||
| 10 | |||
| 11 | #endif | ||
diff --git a/tests/unit/stubs/freertos/event_groups.h b/tests/unit/stubs/freertos/event_groups.h new file mode 100644 index 0000000..28f6403 --- /dev/null +++ b/tests/unit/stubs/freertos/event_groups.h | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | #ifndef STUBS_FREERTOS_EVENT_GROUPS_H | ||
| 2 | #define STUBS_FREERTOS_EVENT_GROUPS_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | |||
| 6 | typedef void *EventGroupHandle_t; | ||
| 7 | #define BIT0 (1 << 0) | ||
| 8 | |||
| 9 | static inline EventGroupHandle_t xEventGroupCreate(void) { return (EventGroupHandle_t)1; } | ||
| 10 | static inline uint32_t xEventGroupSetBits(EventGroupHandle_t eg, uint32_t bits) { (void)eg; return bits; } | ||
| 11 | static inline uint32_t xEventGroupClearBits(EventGroupHandle_t eg, uint32_t bits) { (void)eg; return bits; } | ||
| 12 | |||
| 13 | #endif | ||
diff --git a/tests/unit/stubs/freertos/task.h b/tests/unit/stubs/freertos/task.h new file mode 100644 index 0000000..3855d41 --- /dev/null +++ b/tests/unit/stubs/freertos/task.h | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | #ifndef STUBS_FREERTOS_TASK_H | ||
| 2 | #define STUBS_FREERTOS_TASK_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | #include <stdlib.h> | ||
| 6 | |||
| 7 | typedef void *TaskHandle_t; | ||
| 8 | typedef void *SemaphoreHandle_t; | ||
| 9 | |||
| 10 | static inline void vTaskDelete(TaskHandle_t t) { (void)t; } | ||
| 11 | static inline SemaphoreHandle_t xSemaphoreCreateMutex(void) { return (SemaphoreHandle_t)malloc(1); } | ||
| 12 | static inline void vSemaphoreDelete(SemaphoreHandle_t s) { free(s); } | ||
| 13 | static inline int xSemaphoreTake(SemaphoreHandle_t s, uint32_t blk) { (void)s; (void)blk; return 1; } | ||
| 14 | static inline int xSemaphoreGive(SemaphoreHandle_t s) { (void)s; return 1; } | ||
| 15 | static inline int xTaskCreate(void (*fn)(void*), const char *n, uint32_t st, void *p, uint32_t pri, TaskHandle_t *h) { | ||
| 16 | (void)fn; (void)n; (void)st; (void)p; (void)pri; (void)h; return 1; | ||
| 17 | } | ||
| 18 | |||
| 19 | #endif | ||
diff --git a/tests/unit/stubs/freertos/timers.h b/tests/unit/stubs/freertos/timers.h new file mode 100644 index 0000000..7575807 --- /dev/null +++ b/tests/unit/stubs/freertos/timers.h | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | #ifndef STUBS_FREERTOS_TIMERS_H | ||
| 2 | #define STUBS_FREERTOS_TIMERS_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | |||
| 6 | typedef void *TimerHandle_t; | ||
| 7 | |||
| 8 | static inline TimerHandle_t xTimerCreate(const char *n, uint32_t pd, int ux, void *id, void *cb) { | ||
| 9 | (void)n; (void)pd; (void)ux; (void)id; (void)cb; return (TimerHandle_t)1; | ||
| 10 | } | ||
| 11 | static inline int xTimerStart(TimerHandle_t t, uint32_t blk) { (void)t; (void)blk; return 1; } | ||
| 12 | static inline int xTimerStop(TimerHandle_t t, uint32_t blk) { (void)t; (void)blk; return 1; } | ||
| 13 | static inline void xTimerDelete(TimerHandle_t t, uint32_t blk) { (void)t; (void)blk; } | ||
| 14 | |||
| 15 | #endif | ||
diff --git a/tests/unit/stubs/lwip/ip4_addr.h b/tests/unit/stubs/lwip/ip4_addr.h new file mode 100644 index 0000000..174211b --- /dev/null +++ b/tests/unit/stubs/lwip/ip4_addr.h | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | #ifndef STUBS_LWIP_IP4_ADDR_H | ||
| 2 | #define STUBS_LWIP_IP4_ADDR_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | |||
| 6 | typedef struct { | ||
| 7 | uint32_t addr; | ||
| 8 | } ip4_addr_t; | ||
| 9 | |||
| 10 | typedef ip4_addr_t esp_ip4_addr_t; | ||
| 11 | |||
| 12 | #define IPSTR "%d.%d.%d.%d" | ||
| 13 | #define IP2STR(ip) ((ip)->addr & 0xff), (((ip)->addr >> 8) & 0xff), (((ip)->addr >> 16) & 0xff), (((ip)->addr >> 24) & 0xff) | ||
| 14 | |||
| 15 | static inline void IP4_ADDR(esp_ip4_addr_t *ip, uint8_t a, uint8_t b, uint8_t c, uint8_t d) { | ||
| 16 | ip->addr = ((uint32_t)a) | ((uint32_t)b << 8) | ((uint32_t)c << 16) | ((uint32_t)d << 24); | ||
| 17 | } | ||
| 18 | |||
| 19 | #endif | ||
diff --git a/tests/unit/stubs/lwip/napt.h b/tests/unit/stubs/lwip/napt.h new file mode 100644 index 0000000..c6a5ca1 --- /dev/null +++ b/tests/unit/stubs/lwip/napt.h | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | #ifndef STUBS_LWIP_NAPT_H | ||
| 2 | #define STUBS_LWIP_NAPT_H | ||
| 3 | |||
| 4 | static inline void ip_napt_enable(uint32_t num, int enable) { (void)num; (void)enable; } | ||
| 5 | |||
| 6 | #endif | ||
diff --git a/tests/unit/stubs/lwip/netdb.h b/tests/unit/stubs/lwip/netdb.h new file mode 100644 index 0000000..b71bab8 --- /dev/null +++ b/tests/unit/stubs/lwip/netdb.h | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | #ifndef STUBS_LWIP_NETDB_H | ||
| 2 | #define STUBS_LWIP_NETDB_H | ||
| 3 | |||
| 4 | #endif | ||
diff --git a/tests/unit/stubs/lwip/netif.h b/tests/unit/stubs/lwip/netif.h new file mode 100644 index 0000000..461a64e --- /dev/null +++ b/tests/unit/stubs/lwip/netif.h | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | #ifndef STUBS_LWIP_NETIF_H | ||
| 2 | #define STUBS_LWIP_NETIF_H | ||
| 3 | |||
| 4 | #endif | ||
diff --git a/tests/unit/stubs/lwip/sockets.h b/tests/unit/stubs/lwip/sockets.h new file mode 100644 index 0000000..44f03ac --- /dev/null +++ b/tests/unit/stubs/lwip/sockets.h | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | #ifndef STUBS_LWIP_SOCKETS_H | ||
| 2 | #define STUBS_LWIP_SOCKETS_H | ||
| 3 | |||
| 4 | #endif | ||
diff --git a/tests/unit/stubs/nvs_flash.h b/tests/unit/stubs/nvs_flash.h new file mode 100644 index 0000000..4424a9a --- /dev/null +++ b/tests/unit/stubs/nvs_flash.h | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | #ifndef STUBS_NVS_FLASH_H | ||
| 2 | #define STUBS_NVS_FLASH_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | |||
| 6 | #define ESP_ERR_NVS_NO_FREE_PAGES 0x1101 | ||
| 7 | #define ESP_ERR_NVS_NEW_VERSION_FOUND 0x1102 | ||
| 8 | |||
| 9 | static inline esp_err_t nvs_flash_init(void) { return ESP_OK; } | ||
| 10 | static inline esp_err_t nvs_flash_erase(void) { return ESP_OK; } | ||
| 11 | |||
| 12 | #endif | ||
diff --git a/tests/unit/test_cashu.c b/tests/unit/test_cashu.c new file mode 100644 index 0000000..cec8e08 --- /dev/null +++ b/tests/unit/test_cashu.c | |||
| @@ -0,0 +1,54 @@ | |||
| 1 | #include "test_framework.h" | ||
| 2 | #include "../../main/cashu.h" | ||
| 3 | #include "../../main/config.h" | ||
| 4 | #include <string.h> | ||
| 5 | #include <stdio.h> | ||
| 6 | #include <stdlib.h> | ||
| 7 | |||
| 8 | static tollgate_config_t g_test_config; | ||
| 9 | |||
| 10 | const tollgate_config_t *tollgate_config_get(void) { | ||
| 11 | return &g_test_config; | ||
| 12 | } | ||
| 13 | |||
| 14 | int main(void) | ||
| 15 | { | ||
| 16 | printf("=== test_cashu ===\n"); | ||
| 17 | |||
| 18 | memset(&g_test_config, 0, sizeof(g_test_config)); | ||
| 19 | strncpy(g_test_config.mint_url, "https://testnut.cashu.space", sizeof(g_test_config.mint_url) - 1); | ||
| 20 | g_test_config.price_per_step = 21; | ||
| 21 | g_test_config.step_size_ms = 60000; | ||
| 22 | |||
| 23 | printf("\n--- cashu_calculate_allotment_ms ---\n"); | ||
| 24 | uint64_t a1 = cashu_calculate_allotment_ms(21, 21, 60000); | ||
| 25 | ASSERT_EQ_INT(60000, (int)a1, "21 sats at 21 sats/min = 60000ms"); | ||
| 26 | |||
| 27 | uint64_t a2 = cashu_calculate_allotment_ms(42, 21, 60000); | ||
| 28 | ASSERT_EQ_INT(120000, (int)a2, "42 sats at 21 sats/min = 120000ms"); | ||
| 29 | |||
| 30 | uint64_t a3 = cashu_calculate_allotment_ms(1, 21, 60000); | ||
| 31 | ASSERT_EQ_INT(0, (int)a3, "1 sat at 21 sats/min = 0ms (rounds down)"); | ||
| 32 | |||
| 33 | uint64_t a4 = cashu_calculate_allotment_ms(100, 10, 30000); | ||
| 34 | ASSERT_EQ_INT(300000, (int)a4, "100 sats at 10 sats/30s = 300000ms"); | ||
| 35 | |||
| 36 | printf("\n--- cashu_is_mint_accepted ---\n"); | ||
| 37 | ASSERT(cashu_is_mint_accepted("https://testnut.cashu.space"), "testnut.cashu.space accepted"); | ||
| 38 | ASSERT(!cashu_is_mint_accepted("https://evil.mint.example.com"), "evil mint rejected"); | ||
| 39 | ASSERT(!cashu_is_mint_accepted(""), "empty string rejected"); | ||
| 40 | |||
| 41 | printf("\n--- cashu_decode_token with garbage ---\n"); | ||
| 42 | cashu_token_t token; | ||
| 43 | memset(&token, 0, sizeof(token)); | ||
| 44 | esp_err_t ret = cashu_decode_token("garbage", &token); | ||
| 45 | ASSERT(ret != ESP_OK, "Garbage input returns error"); | ||
| 46 | |||
| 47 | ret = cashu_decode_token("", &token); | ||
| 48 | ASSERT(ret != ESP_OK, "Empty string returns error"); | ||
| 49 | |||
| 50 | ret = cashu_decode_token("cashuA!!invalid-base64!!", &token); | ||
| 51 | ASSERT(ret != ESP_OK, "Invalid base64url returns error"); | ||
| 52 | |||
| 53 | TEST_SUMMARY(); | ||
| 54 | } | ||
diff --git a/tests/unit/test_framework.h b/tests/unit/test_framework.h new file mode 100644 index 0000000..6eb3a10 --- /dev/null +++ b/tests/unit/test_framework.h | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | #ifndef TEST_FRAMEWORK_H | ||
| 2 | #define TEST_FRAMEWORK_H | ||
| 3 | |||
| 4 | #include <stdio.h> | ||
| 5 | #include <stdlib.h> | ||
| 6 | #include <string.h> | ||
| 7 | |||
| 8 | static int g_tests_passed = 0; | ||
| 9 | static int g_tests_failed = 0; | ||
| 10 | |||
| 11 | #define ASSERT(cond, msg) do { \ | ||
| 12 | if (cond) { \ | ||
| 13 | printf(" PASS: %s\n", msg); \ | ||
| 14 | g_tests_passed++; \ | ||
| 15 | } else { \ | ||
| 16 | printf(" FAIL: %s (at %s:%d)\n", msg, __FILE__, __LINE__); \ | ||
| 17 | g_tests_failed++; \ | ||
| 18 | } \ | ||
| 19 | } while(0) | ||
| 20 | |||
| 21 | #define ASSERT_EQ_INT(expected, actual, msg) do { \ | ||
| 22 | int _e = (expected), _a = (actual); \ | ||
| 23 | if (_e == _a) { \ | ||
| 24 | printf(" PASS: %s (got %d)\n", msg, _a); \ | ||
| 25 | g_tests_passed++; \ | ||
| 26 | } else { \ | ||
| 27 | printf(" FAIL: %s (expected %d, got %d) at %s:%d\n", msg, _e, _a, __FILE__, __LINE__); \ | ||
| 28 | g_tests_failed++; \ | ||
| 29 | } \ | ||
| 30 | } while(0) | ||
| 31 | |||
| 32 | #define ASSERT_EQ_STR(expected, actual, msg) do { \ | ||
| 33 | const char *_e = (expected), *_a = (actual); \ | ||
| 34 | if (_e && _a && strcmp(_e, _a) == 0) { \ | ||
| 35 | printf(" PASS: %s (got \"%s\")\n", msg, _a); \ | ||
| 36 | g_tests_passed++; \ | ||
| 37 | } else { \ | ||
| 38 | printf(" FAIL: %s (expected \"%s\", got \"%s\") at %s:%d\n", msg, _e ? _e : "(null)", _a ? _a : "(null)", __FILE__, __LINE__); \ | ||
| 39 | g_tests_failed++; \ | ||
| 40 | } \ | ||
| 41 | } while(0) | ||
| 42 | |||
| 43 | #define ASSERT_MEM_EQ(expected, actual, len, msg) do { \ | ||
| 44 | const uint8_t *_e = (const uint8_t *)(expected), *_a = (const uint8_t *)(actual); \ | ||
| 45 | size_t _l = (len); \ | ||
| 46 | if (_e && _a && memcmp(_e, _a, _l) == 0) { \ | ||
| 47 | printf(" PASS: %s (%zu bytes match)\n", msg, _l); \ | ||
| 48 | g_tests_passed++; \ | ||
| 49 | } else { \ | ||
| 50 | printf(" FAIL: %s (%zu bytes mismatch) at %s:%d\n", msg, _l, __FILE__, __LINE__); \ | ||
| 51 | g_tests_failed++; \ | ||
| 52 | } \ | ||
| 53 | } while(0) | ||
| 54 | |||
| 55 | #define TEST_SUMMARY() do { \ | ||
| 56 | printf("\n=== Results: %d passed, %d failed ===\n", g_tests_passed, g_tests_failed); \ | ||
| 57 | return g_tests_failed > 0 ? 1 : 0; \ | ||
| 58 | } while(0) | ||
| 59 | |||
| 60 | #endif | ||
diff --git a/tests/unit/test_geohash b/tests/unit/test_geohash new file mode 100755 index 0000000..db87d33 --- /dev/null +++ b/tests/unit/test_geohash | |||
| Binary files differ | |||
diff --git a/tests/unit/test_geohash.c b/tests/unit/test_geohash.c new file mode 100644 index 0000000..0da81fa --- /dev/null +++ b/tests/unit/test_geohash.c | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | #include "test_framework.h" | ||
| 2 | #include "../../main/geohash.h" | ||
| 3 | #include <string.h> | ||
| 4 | |||
| 5 | int main(void) | ||
| 6 | { | ||
| 7 | char buf[16]; | ||
| 8 | |||
| 9 | printf("=== test_geohash ===\n"); | ||
| 10 | |||
| 11 | geohash_encode(48.1351, 11.5820, 9, buf); | ||
| 12 | ASSERT_EQ_STR("u281zd9z2", buf, "Munich (48.1351, 11.5820) precision 9"); | ||
| 13 | |||
| 14 | geohash_encode(40.7128, -74.0060, 6, buf); | ||
| 15 | ASSERT(buf[0] == 'd', "NYC starts with 'd'"); | ||
| 16 | ASSERT(buf[1] == 'r', "NYC second char 'r'"); | ||
| 17 | ASSERT_EQ_INT(6, (int)strlen(buf), "NYC precision 6 has length 6"); | ||
| 18 | |||
| 19 | geohash_encode(0.0, 0.0, 8, buf); | ||
| 20 | ASSERT_EQ_STR("s0000000", buf, "Origin (0,0) precision 8"); | ||
| 21 | |||
| 22 | geohash_encode(90.0, 180.0, 5, buf); | ||
| 23 | ASSERT_EQ_INT(5, (int)strlen(buf), "North pole max lon precision 5"); | ||
| 24 | |||
| 25 | geohash_encode(-90.0, -180.0, 5, buf); | ||
| 26 | ASSERT_EQ_INT(5, (int)strlen(buf), "South pole min lon precision 5"); | ||
| 27 | |||
| 28 | geohash_encode(48.1351, 11.5820, 1, buf); | ||
| 29 | ASSERT_EQ_INT(1, (int)strlen(buf), "Precision 1 produces 1 char"); | ||
| 30 | ASSERT(buf[0] == 'u', "Munich precision 1 = 'u'"); | ||
| 31 | |||
| 32 | geohash_encode(48.1351, 11.5820, 4, buf); | ||
| 33 | ASSERT_EQ_STR("u281", buf, "Munich precision 4"); | ||
| 34 | |||
| 35 | char buf2[16]; | ||
| 36 | geohash_encode(48.1351, 11.5820, 9, buf2); | ||
| 37 | ASSERT_EQ_STR("u281zd9z2", buf2, "Munich determinism check"); | ||
| 38 | |||
| 39 | TEST_SUMMARY(); | ||
| 40 | } | ||
diff --git a/tests/unit/test_identity.c b/tests/unit/test_identity.c new file mode 100644 index 0000000..cf4028f --- /dev/null +++ b/tests/unit/test_identity.c | |||
| @@ -0,0 +1,68 @@ | |||
| 1 | #include "test_framework.h" | ||
| 2 | #include "../../main/identity.h" | ||
| 3 | #include <string.h> | ||
| 4 | #include <stdio.h> | ||
| 5 | |||
| 6 | static const char *TEST_NSEC = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"; | ||
| 7 | static const char *TEST_NSEC2 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; | ||
| 8 | |||
| 9 | int main(void) | ||
| 10 | { | ||
| 11 | printf("=== test_identity ===\n"); | ||
| 12 | |||
| 13 | printf("\n--- identity_init with valid nsec ---\n"); | ||
| 14 | esp_err_t ret = identity_init(TEST_NSEC); | ||
| 15 | ASSERT_EQ_INT(ESP_OK, ret, "identity_init returns ESP_OK"); | ||
| 16 | |||
| 17 | const tollgate_identity_t *id = identity_get(); | ||
| 18 | ASSERT(id != NULL, "identity_get returns non-NULL"); | ||
| 19 | ASSERT(id->initialized, "identity is marked initialized"); | ||
| 20 | |||
| 21 | printf("\n--- npub derivation ---\n"); | ||
| 22 | ASSERT_EQ_INT(64, (int)strlen(id->npub_hex), "npub is 64 hex chars"); | ||
| 23 | ASSERT(id->npub_hex[0] != '\0', "npub is not empty"); | ||
| 24 | |||
| 25 | printf("\n--- STA MAC derivation ---\n"); | ||
| 26 | uint8_t expected_sta[] = {0xF2, 0x4D, 0x55, 0x33, 0xDC, 0x9C}; | ||
| 27 | ASSERT_MEM_EQ(expected_sta, id->sta_mac, 6, "STA MAC matches golden vector"); | ||
| 28 | ASSERT_EQ_INT(2, id->sta_mac[0] & 0x02, "STA MAC has locally-administered bit set"); | ||
| 29 | ASSERT_EQ_INT(0, id->sta_mac[0] & 0x01, "STA MAC has multicast bit cleared"); | ||
| 30 | |||
| 31 | printf("\n--- AP MAC derivation ---\n"); | ||
| 32 | uint8_t expected_ap[] = {0x3A, 0x2A, 0xEB, 0xC0, 0xE9, 0xCA}; | ||
| 33 | ASSERT_MEM_EQ(expected_ap, id->ap_mac, 6, "AP MAC matches golden vector"); | ||
| 34 | ASSERT_EQ_INT(2, id->ap_mac[0] & 0x02, "AP MAC has locally-administered bit set"); | ||
| 35 | ASSERT_EQ_INT(0, id->ap_mac[0] & 0x01, "AP MAC has multicast bit cleared"); | ||
| 36 | |||
| 37 | printf("\n--- SSID derivation ---\n"); | ||
| 38 | ASSERT_EQ_STR("TollGate-C0E9CA", id->ap_ssid, "SSID derived from AP MAC last 3 bytes"); | ||
| 39 | |||
| 40 | printf("\n--- AP IP derivation ---\n"); | ||
| 41 | ASSERT_EQ_STR("10.192.45.1", id->ap_ip_str, "AP IP derived from AP MAC bytes"); | ||
| 42 | |||
| 43 | printf("\n--- Determinism ---\n"); | ||
| 44 | ret = identity_init(TEST_NSEC); | ||
| 45 | ASSERT_EQ_INT(ESP_OK, ret, "Second init with same nsec succeeds"); | ||
| 46 | const tollgate_identity_t *id2 = identity_get(); | ||
| 47 | ASSERT_MEM_EQ(id->sta_mac, id2->sta_mac, 6, "STA MAC is deterministic"); | ||
| 48 | ASSERT_MEM_EQ(id->ap_mac, id2->ap_mac, 6, "AP MAC is deterministic"); | ||
| 49 | ASSERT_EQ_STR(id->ap_ssid, id2->ap_ssid, "SSID is deterministic"); | ||
| 50 | |||
| 51 | printf("\n--- Different nsec produces different identity ---\n"); | ||
| 52 | ret = identity_init(TEST_NSEC2); | ||
| 53 | ASSERT_EQ_INT(ESP_OK, ret, "Init with different nsec succeeds"); | ||
| 54 | const tollgate_identity_t *id3 = identity_get(); | ||
| 55 | ASSERT(memcmp(id->sta_mac, id3->sta_mac, 6) != 0, "Different nsec produces different STA MAC"); | ||
| 56 | ASSERT(memcmp(id->ap_mac, id3->ap_mac, 6) != 0, "Different nsec produces different AP MAC"); | ||
| 57 | ASSERT(strcmp(id->ap_ssid, id3->ap_ssid) != 0, "Different nsec produces different SSID"); | ||
| 58 | |||
| 59 | printf("\n--- Invalid nsec ---\n"); | ||
| 60 | ret = identity_init(NULL); | ||
| 61 | ASSERT(ret != ESP_OK, "NULL nsec returns error"); | ||
| 62 | ret = identity_init("tooshort"); | ||
| 63 | ASSERT(ret != ESP_OK, "Short nsec returns error"); | ||
| 64 | ret = identity_init("ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"); | ||
| 65 | ASSERT(ret != ESP_OK, "Invalid hex nsec returns error"); | ||
| 66 | |||
| 67 | TEST_SUMMARY(); | ||
| 68 | } | ||
diff --git a/tests/unit/test_nostr_event.c b/tests/unit/test_nostr_event.c new file mode 100644 index 0000000..12bdb93 --- /dev/null +++ b/tests/unit/test_nostr_event.c | |||
| @@ -0,0 +1,72 @@ | |||
| 1 | #include "test_framework.h" | ||
| 2 | #include "../../main/nostr_event.h" | ||
| 3 | #include "../../main/identity.h" | ||
| 4 | #include <string.h> | ||
| 5 | #include <stdio.h> | ||
| 6 | |||
| 7 | static const char *TEST_NSEC = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"; | ||
| 8 | |||
| 9 | static int time_override = 1700000000; | ||
| 10 | |||
| 11 | int main(void) | ||
| 12 | { | ||
| 13 | printf("=== test_nostr_event ===\n"); | ||
| 14 | |||
| 15 | identity_init(TEST_NSEC); | ||
| 16 | const tollgate_identity_t *id = identity_get(); | ||
| 17 | |||
| 18 | printf("\n--- Event ID computation (NIP-01) ---\n"); | ||
| 19 | nostr_event_t event; | ||
| 20 | esp_err_t ret = nostr_event_init(&event, id->npub_hex, 1, "[]", "Hello TollGate"); | ||
| 21 | ASSERT_EQ_INT(ESP_OK, ret, "nostr_event_init succeeds"); | ||
| 22 | |||
| 23 | ASSERT_EQ_STR(id->npub_hex, event.pubkey, "Event pubkey matches npub"); | ||
| 24 | ASSERT_EQ_INT(1, event.kind, "Event kind is 1"); | ||
| 25 | ASSERT_EQ_INT(64, (int)strlen(event.id), "Event ID is 64 hex chars"); | ||
| 26 | |||
| 27 | printf("\n--- Schnorr signing ---\n"); | ||
| 28 | ret = nostr_event_sign(&event, id->nsec); | ||
| 29 | ASSERT_EQ_INT(ESP_OK, ret, "nostr_event_sign succeeds"); | ||
| 30 | ASSERT_EQ_INT(128, (int)strlen(event.sig), "Signature is 128 hex chars"); | ||
| 31 | ASSERT(event.sig[0] != '\0', "Signature is not empty"); | ||
| 32 | |||
| 33 | printf("\n--- JSON serialization ---\n"); | ||
| 34 | char json_buf[2048]; | ||
| 35 | ret = nostr_event_to_json(&event, json_buf, sizeof(json_buf)); | ||
| 36 | ASSERT_EQ_INT(ESP_OK, ret, "nostr_event_to_json succeeds"); | ||
| 37 | |||
| 38 | ASSERT(strstr(json_buf, "\"id\"") != NULL, "JSON has 'id' field"); | ||
| 39 | ASSERT(strstr(json_buf, "\"pubkey\"") != NULL, "JSON has 'pubkey' field"); | ||
| 40 | ASSERT(strstr(json_buf, "\"created_at\"") != NULL, "JSON has 'created_at' field"); | ||
| 41 | ASSERT(strstr(json_buf, "\"kind\"") != NULL, "JSON has 'kind' field"); | ||
| 42 | ASSERT(strstr(json_buf, "\"tags\"") != NULL, "JSON has 'tags' field"); | ||
| 43 | ASSERT(strstr(json_buf, "\"content\"") != NULL, "JSON has 'content' field"); | ||
| 44 | ASSERT(strstr(json_buf, "\"sig\"") != NULL, "JSON has 'sig' field"); | ||
| 45 | ASSERT(strstr(json_buf, "Hello TollGate") != NULL, "JSON contains content"); | ||
| 46 | |||
| 47 | printf("\n--- Buffer too small ---\n"); | ||
| 48 | char tiny_buf[10]; | ||
| 49 | ret = nostr_event_to_json(&event, tiny_buf, sizeof(tiny_buf)); | ||
| 50 | ASSERT(ret != ESP_OK, "Returns error when buffer too small"); | ||
| 51 | |||
| 52 | printf("\n--- Kind 38787 event (wifistr) ---\n"); | ||
| 53 | nostr_event_t ws_event; | ||
| 54 | const char *ws_tags = "[[\"d\",\"test-npub\"],[\"ssid\",\"TollGate-TEST\"],[\"g\",\"u281w0dfz\"]]"; | ||
| 55 | ret = nostr_event_init(&ws_event, id->npub_hex, 38787, ws_tags, | ||
| 56 | "TollGate WiFi hotspot: TollGate-TEST"); | ||
| 57 | ASSERT_EQ_INT(ESP_OK, ret, "Kind 38787 init succeeds"); | ||
| 58 | ASSERT_EQ_INT(38787, ws_event.kind, "Event kind is 38787"); | ||
| 59 | ASSERT_EQ_INT(64, (int)strlen(ws_event.id), "Kind 38787 event has valid ID"); | ||
| 60 | |||
| 61 | ret = nostr_event_sign(&ws_event, id->nsec); | ||
| 62 | ASSERT_EQ_INT(ESP_OK, ret, "Kind 38787 signing succeeds"); | ||
| 63 | ASSERT_EQ_INT(128, (int)strlen(ws_event.sig), "Kind 38787 signature is 128 hex chars"); | ||
| 64 | |||
| 65 | printf("\n--- Determinism: same input → same ID ---\n"); | ||
| 66 | nostr_event_t event2; | ||
| 67 | nostr_event_init(&event2, id->npub_hex, 1, "[]", "Hello TollGate"); | ||
| 68 | ASSERT(strcmp(event.id, event2.id) == 0 || event.created_at != event2.created_at, | ||
| 69 | "Same input produces same ID (if timestamp matches) or differs only by time"); | ||
| 70 | |||
| 71 | TEST_SUMMARY(); | ||
| 72 | } | ||
diff --git a/tests/unit/test_session.c b/tests/unit/test_session.c new file mode 100644 index 0000000..5b22a62 --- /dev/null +++ b/tests/unit/test_session.c | |||
| @@ -0,0 +1,92 @@ | |||
| 1 | #include "test_framework.h" | ||
| 2 | #include "../../main/session.h" | ||
| 3 | #include "../../main/firewall.h" | ||
| 4 | #include <string.h> | ||
| 5 | #include <stdio.h> | ||
| 6 | |||
| 7 | static uint32_t g_granted_ips[32]; | ||
| 8 | static int g_granted_count = 0; | ||
| 9 | static uint32_t g_revoked_ips[32]; | ||
| 10 | static int g_revoked_count = 0; | ||
| 11 | |||
| 12 | esp_err_t firewall_get_mac_for_ip(uint32_t ip, char *mac_out, size_t size) { | ||
| 13 | (void)ip; | ||
| 14 | snprintf(mac_out, size, "AA:BB:CC:DD:EE:FF"); | ||
| 15 | return 0; | ||
| 16 | } | ||
| 17 | |||
| 18 | void firewall_grant_access(uint32_t ip) { | ||
| 19 | if (g_granted_count < 32) g_granted_ips[g_granted_count++] = ip; | ||
| 20 | } | ||
| 21 | |||
| 22 | void firewall_revoke_access(uint32_t ip) { | ||
| 23 | if (g_revoked_count < 32) g_revoked_ips[g_revoked_count++] = ip; | ||
| 24 | } | ||
| 25 | |||
| 26 | int main(void) | ||
| 27 | { | ||
| 28 | printf("=== test_session ===\n"); | ||
| 29 | |||
| 30 | g_granted_count = 0; | ||
| 31 | g_revoked_count = 0; | ||
| 32 | |||
| 33 | printf("\n--- session_manager_init ---\n"); | ||
| 34 | esp_err_t ret = session_manager_init(); | ||
| 35 | ASSERT_EQ_INT(0, ret, "session_manager_init succeeds"); | ||
| 36 | ASSERT_EQ_INT(0, session_active_count(), "No sessions after init"); | ||
| 37 | |||
| 38 | printf("\n--- session_create ---\n"); | ||
| 39 | const char *secrets[] = {"secret1", "secret2"}; | ||
| 40 | session_t *s = session_create(0x0A01A8C0, 60000, secrets, 2); | ||
| 41 | ASSERT(s != NULL, "session_create returns non-NULL"); | ||
| 42 | ASSERT_EQ_INT(1, session_active_count(), "1 session after create"); | ||
| 43 | ASSERT_EQ_INT(1, g_granted_count, "firewall_grant_access was called"); | ||
| 44 | |||
| 45 | printf("\n--- session_find_by_ip ---\n"); | ||
| 46 | session_t *found = session_find_by_ip(0x0A01A8C0); | ||
| 47 | ASSERT(found == s, "session_find_by_ip returns the created session"); | ||
| 48 | ASSERT(session_find_by_ip(0x01020304) == NULL, "session_find_by_ip returns NULL for unknown IP"); | ||
| 49 | |||
| 50 | printf("\n--- session_is_secret_spent ---\n"); | ||
| 51 | ASSERT(session_is_secret_spent("secret1"), "secret1 is marked spent"); | ||
| 52 | ASSERT(session_is_secret_spent("secret2"), "secret2 is marked spent"); | ||
| 53 | ASSERT(!session_is_secret_spent("secret_unknown"), "unknown secret is not spent"); | ||
| 54 | |||
| 55 | printf("\n--- Duplicate secret rejected ---\n"); | ||
| 56 | const char *dup_secrets[] = {"secret1"}; | ||
| 57 | g_granted_count = 0; | ||
| 58 | session_t *dup = session_create(0x0B01A8C0, 60000, dup_secrets, 1); | ||
| 59 | ASSERT(dup == NULL, "Duplicate secret returns NULL"); | ||
| 60 | ASSERT_EQ_INT(0, g_granted_count, "No new firewall grant for duplicate"); | ||
| 61 | |||
| 62 | printf("\n--- session_extend ---\n"); | ||
| 63 | uint64_t old_allotment = s->allotment_ms; | ||
| 64 | session_extend(s, 30000); | ||
| 65 | ASSERT(s->allotment_ms == old_allotment + 30000, "Allotment extended by 30000ms"); | ||
| 66 | |||
| 67 | printf("\n--- session_revoke ---\n"); | ||
| 68 | g_revoked_count = 0; | ||
| 69 | session_revoke(s); | ||
| 70 | ASSERT_EQ_INT(1, g_revoked_count, "firewall_revoke_access was called"); | ||
| 71 | ASSERT_EQ_INT(0, session_active_count(), "No active sessions after revoke"); | ||
| 72 | |||
| 73 | printf("\n--- session_revoke_all ---\n"); | ||
| 74 | const char *s1[] = {"s1"}; | ||
| 75 | const char *s2[] = {"s2"}; | ||
| 76 | session_create(0x01000001, 60000, s1, 1); | ||
| 77 | session_create(0x01000002, 60000, s2, 1); | ||
| 78 | ASSERT_EQ_INT(2, session_active_count(), "2 sessions created"); | ||
| 79 | |||
| 80 | g_revoked_count = 0; | ||
| 81 | session_revoke_all(); | ||
| 82 | ASSERT_EQ_INT(0, session_active_count(), "No sessions after revoke_all"); | ||
| 83 | |||
| 84 | printf("\n--- session_tick does not crash ---\n"); | ||
| 85 | session_manager_init(); | ||
| 86 | const char *st[] = {"tick_secret"}; | ||
| 87 | session_create(0x0A000001, 60000, st, 1); | ||
| 88 | session_tick(); | ||
| 89 | ASSERT_EQ_INT(1, session_active_count(), "Session still active after tick (not expired)"); | ||
| 90 | |||
| 91 | TEST_SUMMARY(); | ||
| 92 | } | ||