upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-17 01:31:49 +0530
committerYour Name <you@example.com>2026-05-17 01:31:49 +0530
commit347d29658959c7e4b368a15134c183f4ce7a25bc (patch)
tree362b3e40273e3c1435bdd0745de61006041bb803
parent4c47ae188b288e7d24bd9566ab3e6a6805d9484f (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)
-rw-r--r--AGENTS.md195
-rw-r--r--CHECKLIST.md39
-rw-r--r--PLAN.md63
-rw-r--r--main/geohash.c8
-rw-r--r--tests/unit/Makefile66
-rw-r--r--tests/unit/stubs/dhcpserver/dhcpserver.h4
-rw-r--r--tests/unit/stubs/esp_crt_bundle.h6
-rw-r--r--tests/unit/stubs/esp_err.h18
-rw-r--r--tests/unit/stubs/esp_event.h8
-rw-r--r--tests/unit/stubs/esp_http_client.h12
-rw-r--r--tests/unit/stubs/esp_http_server.h10
-rw-r--r--tests/unit/stubs/esp_log.h10
-rw-r--r--tests/unit/stubs/esp_mac.h19
-rw-r--r--tests/unit/stubs/esp_netif.h17
-rw-r--r--tests/unit/stubs/esp_spiffs.h15
-rw-r--r--tests/unit/stubs/esp_system.h4
-rw-r--r--tests/unit/stubs/esp_tls.h25
-rw-r--r--tests/unit/stubs/esp_wifi.h40
-rw-r--r--tests/unit/stubs/freertos/FreeRTOS.h11
-rw-r--r--tests/unit/stubs/freertos/event_groups.h13
-rw-r--r--tests/unit/stubs/freertos/task.h19
-rw-r--r--tests/unit/stubs/freertos/timers.h15
-rw-r--r--tests/unit/stubs/lwip/ip4_addr.h19
-rw-r--r--tests/unit/stubs/lwip/napt.h6
-rw-r--r--tests/unit/stubs/lwip/netdb.h4
-rw-r--r--tests/unit/stubs/lwip/netif.h4
-rw-r--r--tests/unit/stubs/lwip/sockets.h4
-rw-r--r--tests/unit/stubs/nvs_flash.h12
-rw-r--r--tests/unit/test_cashu.c54
-rw-r--r--tests/unit/test_framework.h60
-rwxr-xr-xtests/unit/test_geohashbin0 -> 20744 bytes
-rw-r--r--tests/unit/test_geohash.c40
-rw-r--r--tests/unit/test_identity.c68
-rw-r--r--tests/unit/test_nostr_event.c72
-rw-r--r--tests/unit/test_session.c92
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
5TollGate 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
23Identity (SSID, IP, MAC) is derived from `nsec` in config.json. Each board gets a unique nsec.
24
25## Boot Sequence
26
27```
28nvs_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)
123make test-unit
124
125# Integration tests (needs Board A connected and flashed)
126export TOLLGATE_IP=10.192.45.1
127export TOLLGATE_SSID=TollGate-C0E9CA
128make test-integration
129
130# E2E tests (needs Board A + browser)
131make test-e2e
132
133# All tests
134make test-all
135
136# Quick smoke (30s, needs hardware)
137make smoke
138```
139
140## Build & Flash
141
142```bash
143source ~/esp/esp-idf/export.sh
144make flash # build + flash to Board A
145make flash-a # same
146make 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
diff --git a/PLAN.md b/PLAN.md
index 0fcecac..8ea827d 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -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
399Host-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
415Node.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
429Playwright 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 @@
1REPO_ROOT := ../..
2SECP256K1_SRC := $(REPO_ROOT)/nucula_src/components/secp256k1/libsecp256k1
3SECP256K1_INC := $(SECP256K1_SRC)/include
4SECP256K1_PRIV_INC := $(SECP256K1_SRC)/src
5SECP256K1_CFG := $(REPO_ROOT)/nucula_src/components/secp256k1
6CJSON_SRC := $(REPO_ROOT)/../esp/esp-idf/components/json/cJSON
7
8CC := gcc
9CFLAGS := -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
20LDFLAGS := -lmbedcrypto -lcjson
21
22SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o
23
24TESTS := test_geohash test_identity test_nostr_event test_cashu test_session
25
26.PHONY: all test clean $(TESTS)
27
28all: test
29
30test: $(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
50test_geohash: test_geohash.c $(REPO_ROOT)/main/geohash.c
51 $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
52
53test_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
56test_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
59test_cashu: test_cashu.c $(REPO_ROOT)/main/cashu.c
60 $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/cashu.c -o $@ $(LDFLAGS)
61
62test_session: test_session.c $(REPO_ROOT)/main/session.c
63 $(CC) $(CFLAGS) $< $(REPO_ROOT)/main/session.c -o $@ $(LDFLAGS)
64
65clean:
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
4static 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
8typedef 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
6static 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
6typedef void *esp_http_client_handle_t;
7
8typedef 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
7typedef void *httpd_handle_t;
8typedef 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
7static 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
6typedef 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
13static 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
6typedef 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
13static 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
6typedef struct esp_tls esp_tls_t;
7
8typedef struct {
9 void *crt_bundle_attach;
10 int use_global_ca_store;
11} esp_tls_cfg_t;
12
13static inline esp_tls_t *esp_tls_init(void) { return (esp_tls_t*)1; }
14static 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}
17static 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}
20static 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}
23static 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
16typedef 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
35static inline esp_err_t esp_wifi_set_mac(int ifx, const uint8_t *mac) { (void)ifx; (void)mac; return ESP_OK; }
36static inline esp_err_t esp_wifi_set_config(int ifx, const wifi_config_t *cfg) { (void)ifx; (void)cfg; return ESP_OK; }
37static inline esp_err_t esp_wifi_set_mode(uint8_t mode) { (void)mode; return ESP_OK; }
38static 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
6static inline uint32_t xTaskGetTickCount(void) { return 0; }
7static 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
6typedef void *EventGroupHandle_t;
7#define BIT0 (1 << 0)
8
9static inline EventGroupHandle_t xEventGroupCreate(void) { return (EventGroupHandle_t)1; }
10static inline uint32_t xEventGroupSetBits(EventGroupHandle_t eg, uint32_t bits) { (void)eg; return bits; }
11static 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
7typedef void *TaskHandle_t;
8typedef void *SemaphoreHandle_t;
9
10static inline void vTaskDelete(TaskHandle_t t) { (void)t; }
11static inline SemaphoreHandle_t xSemaphoreCreateMutex(void) { return (SemaphoreHandle_t)malloc(1); }
12static inline void vSemaphoreDelete(SemaphoreHandle_t s) { free(s); }
13static inline int xSemaphoreTake(SemaphoreHandle_t s, uint32_t blk) { (void)s; (void)blk; return 1; }
14static inline int xSemaphoreGive(SemaphoreHandle_t s) { (void)s; return 1; }
15static 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
6typedef void *TimerHandle_t;
7
8static 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}
11static inline int xTimerStart(TimerHandle_t t, uint32_t blk) { (void)t; (void)blk; return 1; }
12static inline int xTimerStop(TimerHandle_t t, uint32_t blk) { (void)t; (void)blk; return 1; }
13static 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
6typedef struct {
7 uint32_t addr;
8} ip4_addr_t;
9
10typedef 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
15static 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
4static 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
9static inline esp_err_t nvs_flash_init(void) { return ESP_OK; }
10static 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
8static tollgate_config_t g_test_config;
9
10const tollgate_config_t *tollgate_config_get(void) {
11 return &g_test_config;
12}
13
14int 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
8static int g_tests_passed = 0;
9static 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
5int 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
6static const char *TEST_NSEC = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
7static const char *TEST_NSEC2 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
8
9int 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
7static const char *TEST_NSEC = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
8
9static int time_override = 1700000000;
10
11int 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
7static uint32_t g_granted_ips[32];
8static int g_granted_count = 0;
9static uint32_t g_revoked_ips[32];
10static int g_revoked_count = 0;
11
12esp_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
18void firewall_grant_access(uint32_t ip) {
19 if (g_granted_count < 32) g_granted_ips[g_granted_count++] = ip;
20}
21
22void firewall_revoke_access(uint32_t ip) {
23 if (g_revoked_count < 32) g_revoked_ips[g_revoked_count++] = ip;
24}
25
26int 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}