upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md4
-rw-r--r--CHECKLIST.md191
-rw-r--r--Makefile91
-rw-r--r--package.json20
-rw-r--r--tests/e2e/captive-portal.spec.mjs (renamed from tests/captive-portal.spec.mjs)2
-rw-r--r--tests/e2e/interop-happy-path.spec.mjs (renamed from tests/interop-happy-path.spec.mjs)0
-rw-r--r--tests/e2e/playwright.config.mjs (renamed from tests/playwright.config.mjs)2
-rw-r--r--tests/helpers/network.mjs2
-rw-r--r--tests/integration/api.mjs (renamed from tests/api.mjs)0
-rw-r--r--tests/integration/network.mjs (renamed from tests/network.mjs)2
-rw-r--r--tests/integration/phase2.mjs (renamed from tests/phase2.mjs)2
-rw-r--r--tests/integration/smoke.mjs (renamed from tests/smoke.mjs)2
-rw-r--r--tests/integration/test-dns-firewall.mjs123
-rw-r--r--tests/integration/test-reset-auth.mjs101
-rw-r--r--tests/integration/test-session-expiry.mjs103
15 files changed, 479 insertions, 166 deletions
diff --git a/AGENTS.md b/AGENTS.md
index f5d4f7e..6f1c399 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -51,8 +51,8 @@ nvs_flash_init()
51- `wifistr.c/h` — kind 38787 event builder + WebSocket relay publish 51- `wifistr.c/h` — kind 38787 event builder + WebSocket relay publish
52- `captive_portal.c/h` — HTTP :80 portal, captive detection, grant/reset 52- `captive_portal.c/h` — HTTP :80 portal, captive detection, grant/reset
53- `dns_server.c/h` — DNS hijack/forward per-client, DoT reject 53- `dns_server.c/h` — DNS hijack/forward per-client, DoT reject
54- `firewall.c/h` — NAPT on/off per-client, MAC resolution 54- `firewall.c/h` — per-client NAT filter via LWIP_HOOK_IP4_CANFORWARD, MAC resolution
55- `session.c/h` — time-based sessions, spent-secret tracking 55- `session.c/h` — time-based sessions, MAC tracking
56- `cashu.c/h` — Cashu token decode, checkstate, allotment calc 56- `cashu.c/h` — Cashu token decode, checkstate, allotment calc
57- `tollgate_api.c/h` — HTTP :2121, payment endpoints, wallet endpoints 57- `tollgate_api.c/h` — HTTP :2121, payment endpoints, wallet endpoints
58 58
diff --git a/CHECKLIST.md b/CHECKLIST.md
index b71bd14..c5dfbe4 100644
--- a/CHECKLIST.md
+++ b/CHECKLIST.md
@@ -22,158 +22,115 @@
22- [x] Tests 1-14: ALL PASSING 22- [x] Tests 1-14: ALL PASSING
23 23
24## Phase 2: E-Cash Payments — COMPLETE 24## Phase 2: E-Cash Payments — COMPLETE
25### Code Written 25- [x] Implement cashu.c/h, session.c/h, tollgate_api.c/h
26- [x] Implement cashu.c/h (Cashu token parse, base64url, checkstate, mint validation) 26- [x] Update captive portal HTML with payment form
27- [x] Implement session.c/h (time-based allotment, expiry, secret tracking, MAC tracking) 27- [x] Wire into tollgate_main.c
28- [x] Implement tollgate_api.c/h (:2121 server, GET/POST /, /usage, /whoami) 28- [x] Per-MAC access tracking, two httpd instances
29- [x] Update captive portal HTML with payment form (Cashu token textarea + "Pay & Connect") 29- [x] Bug fixes: stack overflow, heap allocations, TLS, token decode
30- [x] Wire into tollgate_main.c (session_init, api_start, session_tick loop)
31- [x] Per-MAC access tracking: `firewall_get_mac_for_ip()` using `esp_wifi_ap_get_sta_list_with_ip()` + ARP fallback
32- [x] Two httpd instances: port 80 (captive portal) and port 2121 (TollGate API)
33
34### Bug Fixes
35- [x] Stack overflow: httpd stack_size increased to 32768 (TLS+mbedTLS needs ~20KB)
36- [x] Heap allocations: cashu_token_t, cashu_proof_state_t, json_buf, post_body all heap-allocated
37- [x] TLS to mint: `esp_crt_bundle_attach` + `esp-tls` in CMakeLists.txt REQUIRES
38- [x] HTTP client: `open/write/fetch_headers/read` pattern (not `perform`)
39- [x] Token decode: dynamic `json_buf` sizing `malloc((b64_len * 3) / 4 + 4)`, strip trailing `\n`/`\r`
40- [x] POST body recv: loop `httpd_req_recv` until all `content_len` bytes read
41- [x] `secret_count` bug: capped at `MIN(proof_count, 5)` before `session_create`
42- [x] `config.c` default mint URL fixed to `testnut.cashu.space`
43- [x] Makefile: nutshell wallet targets (wallet-setup, wallet-info, mint-token, send-token)
44- [x] `tests/phase2.mjs`: `/whoami` test checks `includes('mac=')`
45
46### Tests Passing
47- [x] Tests 15-24: ALL PASSING 30- [x] Tests 15-24: ALL PASSING
48 31
49## Phase 3: On-Device Wallet + Nostr Identity + Wifistr — COMPLETE 32## Phase 3: On-Device Wallet + Nostr Identity + Wifistr — COMPLETE
50### nucula Wallet Integration 33- [x] nucula wallet integration (git submodule, C++ bridge, C API)
51- [x] Add nucula as git submodule (`nucula_src/`) 34- [x] Nostr identity derivation (HMAC-SHA512, MAC/SSID/IP)
52- [x] Create `components/secp256k1/` (symlink to nucula's libsecp256k1) 35- [x] Nostr event signing (NIP-01, Schnorr)
53- [x] Create `components/nucula_lib/` (C++ bridge + C API) 36- [x] Geohash encoding
54- [x] C bridge: `nucula_wallet.h` (init, receive, send, swap_all, balance, proofs_json) 37- [x] Wifistr service discovery (kind 38787)
55- [x] All wallet operations tested on Board A: pay, swap, send, persistence 38- [x] 58 unit tests passing
56
57### Nostr Identity Derivation (identity.c/h)
58- [x] HMAC-SHA512 derivation via mbedtls, npub via secp256k1
59- [x] Derive STA/AP MAC, SSID, AP IP from nsec
60- [x] Set MACs via `esp_wifi_set_mac()` in boot sequence
61- [x] 24/24 unit tests passing
62
63### Nostr Event Signing (nostr_event.c/h)
64- [x] NIP-01 canonical JSON, SHA-256 ID, Schnorr signature
65- [x] 23/23 unit tests passing
66
67### Geohash Encoding (geohash.c/h)
68- [x] Standard base-32 geohash encoding
69- [x] 11/11 unit tests passing
70
71### Wifistr Service Discovery (wifistr.c/h)
72- [x] kind 38787 event builder + WebSocket relay publish
73- [x] Publish on boot + periodic timer (6h default)
74- [x] Verified published to relay.damus.io and nos.lol
75 39
76## Phase 4: ESP32 TollGate Client Detection + Auto-Payment — COMPLETE (commit `78dd599`) 40## Phase 4: ESP32 TollGate Client Detection + Auto-Payment — COMPLETE (commit `78dd599`)
77- [x] tollgate_client.c/h — detection, payment, monitoring, state machine 41- [x] tollgate_client.c/h — detection, payment, monitoring, state machine
78- [x] Config fields: client_enabled, client_steps_to_buy, etc.
79- [x] Integration into tollgate_main.c
80- [x] 30/30 unit tests passing 42- [x] 30/30 unit tests passing
81 43
82## Phase 5: Lightning Auto-Payout — COMPLETE (commit `cb4bd7d`) 44## Phase 5: Lightning Auto-Payout — COMPLETE (commit `cb4bd7d`)
83- [x] lnurl_pay.c/h — LNURL-pay HTTP flow 45- [x] lnurl_pay.c/h, lightning_payout.c/h, nucula_wallet_melt()
84- [x] lightning_payout.c/h — periodic balance check, threshold, multi-recipient split, melt 46- [x] 18 unit tests passing
85- [x] nucula_wallet_melt() bridge for NUT-05
86- [x] Config: payout.enabled, recipients, mints, fee_tolerance, etc.
87- [x] 7/7 lnurl_pay + 11/11 lightning_payout = 18 unit tests passing
88 47
89## Phase 6: Bytes-Based Billing — COMPLETE (commit `edd125d`) 48## Phase 6: Bytes-Based Billing — COMPLETE (commit `edd125d`)
90- [x] Dual-metric session support (milliseconds + bytes) 49- [x] Dual-metric session support (milliseconds + bytes)
91- [x] session_create_bytes(), session_add_bytes()
92- [x] Config: metric, step_size_bytes
93- [x] Discovery endpoint advertises correct metric
94- [x] Unit tests: bytes session lifecycle, mixed metrics
95 50
96## Phase 7: MCP Handler + NIP-04 + CVM Server — COMPLETE (commit `fdf662f`) 51## Phase 7: MCP Handler + NIP-04 + CVM Server — COMPLETE (commit `fdf662f`)
97- [x] mcp_handler.c/h — 4 tools (get_config, set_config, get_balance, wallet_send), 25 unit tests 52- [x] mcp_handler.c/h (4 tools, 25 unit tests)
98- [x] nip04.c/h — AES-256-CBC + ECDH with 0x02 compressed pubkey prefix, 15 unit tests 53- [x] nip04.c/h (AES-256-CBC + ECDH, 15 unit tests)
99- [x] cvm_server.c/h — Nostr DM listener skeleton with FreeRTOS task 54- [x] cvm_server.c/h (Nostr DM listener)
100- [x] Fixed NIP-04 IV bug: mbedtls_aes_crypt_cbc modifies IV in-place
101- [x] Fixed missing esp_random.h include in nip04.c
102- [x] 156 total unit tests passing across 10 test binaries
103 55
104## Bug Fixes — COMPLETE (commit `3342c8e`) 56## Bug Fixes — COMPLETE (commit `3342c8e`)
105- [x] reset_auth_handler now calls session_revoke_all() before firewall_revoke_all() 57- [x] reset_auth, /usage, metric default, sys_evt stack overflow fixes
106- [x] Port 80 /usage shows real session data (remaining/total) instead of "0/0"
107- [x] Config metric defaults to "milliseconds" (ESP32 can't track per-client bytes from NAT)
108- [x] Fixed sys_evt stack overflow: deferred start_services() to dedicated 32KB task
109 58
110## Playwright Interop Tests — COMPLETE (commit `4fb44e7`) 59## Playwright Interop Tests — COMPLETE (commit `4fb44e7`)
111- [x] 18/18 tests passing (11 ESP32 + 7 ESP32↔OpenWRT interop) 60- [x] 18/18 tests passing (11 ESP32 + 7 ESP32↔OpenWRT interop)
112- [x] 7 screenshots generated 61
113- [x] Double-spend rejection verified on live hardware 62## Per-Client NAT Filtering — COMPLETE (commit `0c2c67b`)
63- [x] Create `main/lwip_tollgate_hooks.h` — LWIP_HOOK_IP4_CANFORWARD definition
64- [x] Update `CMakeLists.txt` — inject hook header into lwIP compilation
65- [x] Add `tollgate_ip4_canforward_filter()` to `firewall.c` — filter by source IP, network byte order
66- [x] NAT always ON, per-client filter in lwIP forwarding path
67- [x] Remove `update_nat()`, `firewall_enable_nat()`, `firewall_disable_nat()`
68- [x] Subnet-aware: only filter AP subnet packets, allow internet responses
69- [x] Fix byte order bug: firewall stores IPs in network byte order
70- [x] Reduce API server stack 32KB→16KB (fixes ESP_ERR_HTTPD_TASK)
71- [x] E2E verified: block→pay→allow→revoke→block on live hardware
72
73## Spent-Secret Cleanup — COMPLETE (commit `0c2c67b`)
74- [x] Remove `s_spent_secrets[]`, `session_is_secret_spent()` from session.c
75- [x] Remove `spent_secrets`/`spent_secret_count` from `session_t`
76- [x] Remove spent-secret params from `session_create()`/`session_create_bytes()`
77- [x] Remove local spent-secret check in `tollgate_api.c`
78- [x] Update `tests/unit/test_session.c`
79- [x] 186 unit tests passing
114 80
115--- 81---
116 82
117## TODO — In Progress 83## TODO — Remaining
118
119### Per-Client NAT Filtering (Multi-Client Fix)
120- [ ] Create `main/lwip_tollgate_hooks.h` — LWIP_HOOK_IP4_CANFORWARD definition
121- [ ] Update `CMakeLists.txt` — inject hook header into lwIP compilation
122- [ ] Add `tollgate_ip4_canforward_filter()` to `firewall.c` — filter forwarded packets by source IP
123- [ ] Change firewall strategy: NAT always ON, per-client filter in lwIP forwarding path
124- [ ] Remove `update_nat()`, `firewall_enable_nat()`, `firewall_disable_nat()` from firewall.c
125- [ ] Update `stop_services()` in tollgate_main.c — remove `firewall_disable_nat()` call
126- [ ] Add unit test for filter function
127- [ ] Build, flash, test on Board A
128- [ ] Verify multi-client isolation: expire one client while other is active
129
130### Spent-Secret Cleanup
131- [ ] Remove `s_spent_secrets[]` and `session_is_secret_spent()` from `session.c`
132- [ ] Remove `spent_secrets` field from `session_t` struct in `session.h`
133- [ ] Remove `spent_secrets` params from `session_create()` and `session_create_bytes()`
134- [ ] Remove local spent-secret check in `tollgate_api.c` (lines 227-239)
135- [ ] Remove `secrets[]` array construction in `tollgate_api.c`
136- [ ] Update `tests/unit/test_session.c` — remove secret-tracking tests
137- [ ] Run `make test-unit` — all tests pass
138
139### Integration Tests (tests/integration/)
140- [ ] Create `tests/integration/` directory
141- [ ] Move existing tests (api.mjs, network.mjs, smoke.mjs, phase2.mjs) into integration/
142- [ ] Write `test-reset-auth.mjs` — verify sessions cleared after reset
143- [ ] Write `test-session-lifecycle.mjs` — pay → verify usage → wait expiry → verify blocked (65s)
144- [ ] Write `test-dns-firewall.mjs` — DNS hijack before auth, forward after auth
145- [ ] Update Makefile targets for new paths
146- [ ] All integration tests passing
147 84
148### Test Reorganization 85### Test Reorganization
149- [ ] Fix all hardcoded IPs → `process.env.TOLLGATE_IP` 86- [ ] Fix hardcoded IP fallbacks: `192.168.4.1` → `10.192.45.1` in test files
150- [ ] Move `tests/captive-portal.spec.mjs` → `tests/e2e/` 87- [ ] Create `tests/integration/` and `tests/e2e/` directories
151- [ ] Move `tests/interop-happy-path.spec.mjs` → `tests/e2e/` or `tests/integration/` 88- [ ] Move `api.mjs`, `network.mjs`, `phase2.mjs`, `smoke.mjs` → `tests/integration/`
152- [ ] Move `tests/playwright.config.mjs` → `tests/e2e/` 89- [ ] Move `captive-portal.spec.mjs`, `interop-happy-path.spec.mjs` → `tests/e2e/`
90- [ ] Move `playwright.config.mjs` → `tests/e2e/`
91
92### New Integration Tests
93- [ ] Write `tests/integration/test-reset-auth.mjs` — reset → verify blocked → pay → verify allowed → reset → verify blocked
94- [ ] Write `tests/integration/test-session-expiry.mjs` — pay → wait 65s → verify blocked (slow test)
95- [ ] Write `tests/integration/test-dns-firewall.mjs` — DNS hijack before auth, forward after auth, per-client NAT filter
96
97### Makefile & Package Updates
98- [ ] Add `test-unit`, `test-integration`, `test-e2e`, `test-all`, `test-session-expiry` targets
99- [ ] Update `package.json` scripts for new paths
100- [ ] Update existing targets to new paths
153 101
154### Playwright Video Recording Fix 102### Playwright Video Recording Fix
155- [ ] Per-test context isolation (not shared serial context) 103- [ ] Per-test context isolation in playwright.config.mjs
156- [ ] Verify `.webm` files generated in test-results/ 104- [ ] Verify `.webm` files generated in `tests/e2e/test-results/`
157 105
158### OpenWRT Interop 106### AGENTS.md Update
159- [ ] Investigate `nofee.testnut.cashu.space` API compatibility issues 107- [ ] Update firewall description: "per-client NAT filter via LWIP_HOOK_IP4_CANFORWARD"
160- [ ] Fix cashu CLI v0.19.2 Pydantic validation failures with missing `active` field 108- [ ] Update session.c description: remove "spent-secret tracking"
161 109
162### Board B 110### OpenWRT Interop
163- [ ] Flash Board B with current firmware (different nsec) 111- [ ] SSH to `root@10.47.41.1`, verify `tollgate-wrt` still running
164- [ ] Cross-board payment test: Board B → Board A 112- [ ] Test `curl http://10.47.41.1:2121/` — kind=10021 response
165- [ ] ESP32→ESP32 auto-payment (Scenario 5) 113- [ ] Investigate `nofee.testnut.cashu.space` API compatibility
114- [ ] Document findings
115
116### Board B — Flash + Cross-Board Test
117- [x] Generate nsec for Board B: `9af47906b45aca5e238390f3d03c8274e154198e81aa2095065627d1e61ca968`
118- [x] Derived identity: SSID `TollGate-b96d80`, AP IP `10.185.47.1`, AP MAC `fe:08:f7:b9:6d:80`
119- [ ] Create Board B config.json with new nsec
120- [ ] Flash Board B at `/dev/ttyACM1`
121- [ ] Verify Board B boots with different SSID/IP
122- [ ] Cross-board payment test: Board B pays Board A (Scenario 5)
166 123
167--- 124---
168 125
169## Reminders 126## Reminders
170- **Commit + push every time a test passes that previously didn't pass** 127- **Commit + push every time a test passes that previously didn't pass**
171- Board A: `/dev/ttyACM0`, MAC `94:a9:90:2e:37:7c`, SSID `TollGate-C0E9CA`, AP IP `10.192.45.1` 128- Board A: `/dev/ttyACM0`, SSID `TollGate-C0E9CA`, AP IP `10.192.45.1`
172- Board B: `/dev/ttyACM1`, MAC `fc:01:2c:c5:50:50` 129- Board B: `/dev/ttyACM1`, SSID `TollGate-b96d80`, AP IP `10.185.47.1`, nsec `9af47906...`
173- OpenWRT Router: SSH `root@10.47.41.1`, port 2121 130- OpenWRT Router: SSH `root@10.47.41.1`, port 2121
174- `source ~/esp/esp-idf/export.sh` before `idf.py` 131- `source ~/esp/esp-idf/export.sh` before `idf.py`
175- Latest commit: `3342c8e` 132- Latest commit: `0c2c67b`
176- 156 unit tests + 18 Playwright tests — all passing 133- 186 unit tests + 18 Playwright tests — all passing
177- sudo password: `c03rad0r123` 134- sudo password: `c03rad0r123`
178- Token generation: `cashu -h https://testnut.cashu.space send --legacy 21` 135- Token generation: `cashu -h https://testnut.cashu.space send --legacy 21`
179- See `AGENTS.md` for full testing rules 136- See `AGENTS.md` for full testing rules
diff --git a/Makefile b/Makefile
index 2ed8e07..40f0e7b 100644
--- a/Makefile
+++ b/Makefile
@@ -17,12 +17,15 @@ NODE ?= node
17NPM ?= npm 17NPM ?= npm
18PYTHON ?= python3 18PYTHON ?= python3
19 19
20TOLLGATE_IP ?= 10.192.45.1
21
20.PHONY: help setup detect-ports detect-chip detect-all 22.PHONY: help setup detect-ports detect-chip detect-all
21.PHONY: flash flash-a flash-b monitor monitor-a monitor-b 23.PHONY: flash flash-a flash-b monitor monitor-a monitor-b
22.PHONY: test smoke test-api test-portal test-network test-full 24.PHONY: test test-unit test-integration test-e2e test-all
23.PHONY: tokens test-payment wallet-setup wallet-info wallet-balance mint-token send-token 25.PHONY: test-smoke test-api test-network test-portal test-payment
24.PHONY: clean erase-nvs reset serial-log 26.PHONY: test-reset-auth test-session-expiry test-dns-firewall
25.PHONY: bootstrap-config 27.PHONY: tokens wallet-setup wallet-info wallet-balance mint-token send-token
28.PHONY: clean erase-nvs reset serial-log bootstrap-config
26 29
27help: 30help:
28 @echo "TollGate ESP32 — Makefile" 31 @echo "TollGate ESP32 — Makefile"
@@ -38,25 +41,24 @@ help:
38 @echo " flash-b Flash to PORT_B" 41 @echo " flash-b Flash to PORT_B"
39 @echo " monitor Serial monitor on PORT" 42 @echo " monitor Serial monitor on PORT"
40 @echo "" 43 @echo ""
41 @echo "Test (Phase 1):" 44 @echo "Testing:"
42 @echo " test Run all Phase 1 tests" 45 @echo " test-unit Host C unit tests (no hardware)"
43 @echo " smoke Quick 30s smoke test" 46 @echo " test-integration Node.js integration tests (live board)"
44 @echo " test-api curl API endpoint tests" 47 @echo " test-e2e Playwright browser E2E tests"
45 @echo " test-portal Playwright captive portal tests" 48 @echo " test-all Run all three test layers"
46 @echo " test-network DNS/NAT connectivity tests" 49 @echo " test-smoke Quick 30s smoke test"
47 @echo " test-full All 14 Phase 1 tests" 50 @echo " test-reset-auth Reset auth + per-client NAT filter test"
51 @echo " test-dns-firewall DNS hijack + NAT filter test"
52 @echo " test-session-expiry Session lifecycle with 65s expiry wait"
48 @echo "" 53 @echo ""
49 @echo "Test (Phase 2):" 54 @echo "Wallet:"
50 @echo " wallet-setup Initialize nutshell wallet for test mint" 55 @echo " wallet-setup Initialize nutshell wallet for test mint"
51 @echo " wallet-info Show mint info" 56 @echo " wallet-info Show mint info"
52 @echo " wallet-balance Show wallet balance" 57 @echo " wallet-balance Show wallet balance"
53 @echo " mint-token Invoice + send test token (AMOUNT=21)" 58 @echo " mint-token Invoice + send test token (AMOUNT=21)"
54 @echo " send-token Send cashuA token (AMOUNT=21)" 59 @echo " send-token Send cashuA token (AMOUNT=21)"
55 @echo " tokens Alias for send-token"
56 @echo " test-payment Payment flow tests"
57 @echo "" 60 @echo ""
58 @echo "Utilities:" 61 @echo "Utilities:"
59 @echo " setup One-time: install esptool, deps"
60 @echo " clean Clean build" 62 @echo " clean Clean build"
61 @echo " erase-nvs Erase NVS partition on PORT" 63 @echo " erase-nvs Erase NVS partition on PORT"
62 @echo " reset Hardware reset on PORT" 64 @echo " reset Hardware reset on PORT"
@@ -144,33 +146,60 @@ monitor-b: PORT=$(PORT_B)
144monitor-b: monitor 146monitor-b: monitor
145 147
146# ────────────────────────────────────────────── 148# ──────────────────────────────────────────────
147# Test Infrastructure 149# Testing
148# ────────────────────────────────────────────── 150# ──────────────────────────────────────────────
149 151
150test: test-api test-network 152test-unit:
151 @echo "=== All tests passed ===" 153 @echo "=== Running host unit tests ==="
154 $(MAKE) -C tests/unit test
155
156test-integration: test-api test-network test-reset-auth test-dns-firewall
157 @echo "=== Integration tests passed ==="
158
159test-e2e:
160 @echo "=== Running Playwright E2E tests ==="
161 cd tests/e2e && npx playwright test
162
163test-all: test-unit test-integration test-e2e
164 @echo "=== All test layers passed ==="
165
166test: test-unit test-integration
167 @echo "=== Tests passed ==="
152 168
153smoke: 169test-smoke:
154 @echo "=== Running smoke test (30s) ===" 170 @echo "=== Running smoke test (30s) ==="
155 $(NODE) tests/smoke.mjs $(PORT) 171 TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/smoke.mjs
156 172
157test-api: 173test-api:
158 @echo "=== Running API tests ===" 174 @echo "=== Running API tests ==="
159 $(NODE) tests/api.mjs 175 TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/api.mjs
176
177test-network:
178 @echo "=== Running network tests ==="
179 TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/network.mjs
160 180
161test-portal: 181test-portal:
162 @echo "=== Running Playwright portal tests ===" 182 @echo "=== Running Playwright portal tests ==="
163 npx playwright test tests/captive-portal.spec.mjs 183 cd tests/e2e && npx playwright test captive-portal.spec.mjs
164 184
165test-network: 185test-payment:
166 @echo "=== Running network tests ===" 186 @echo "=== Running payment tests ==="
167 $(NODE) tests/network.mjs 187 TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/phase2.mjs
188
189test-reset-auth:
190 @echo "=== Running reset auth test ==="
191 TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-reset-auth.mjs
168 192
169test-full: test-api test-portal test-network 193test-session-expiry:
170 @echo "=== Full test suite passed ===" 194 @echo "=== Running session expiry test (65s wait, ~80s total) ==="
195 TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-session-expiry.mjs
196
197test-dns-firewall:
198 @echo "=== Running DNS + firewall test ==="
199 TOLLGATE_IP=$(TOLLGATE_IP) $(NODE) tests/integration/test-dns-firewall.mjs
171 200
172# ────────────────────────────────────────────── 201# ──────────────────────────────────────────────
173# Phase 2: Payment Testing (Nutshell wallet) 202# Wallet
174# ────────────────────────────────────────────── 203# ──────────────────────────────────────────────
175 204
176wallet-setup: 205wallet-setup:
@@ -187,8 +216,8 @@ wallet-balance:
187 cashu --env-mint $(TEST_MINT) balance 216 cashu --env-mint $(TEST_MINT) balance
188 217
189mint-token: 218mint-token:
190 @echo "=== Minting test token (AMOUNT=$(or $(AMOUNT),21)) ==="
191 @AMOUNT=$${AMOUNT:-21}; \ 219 @AMOUNT=$${AMOUNT:-21}; \
220 echo "=== Minting test token ($$AMOUNT sats) ==="; \
192 cashu --env-mint $(TEST_MINT) invoice $$AMOUNT && \ 221 cashu --env-mint $(TEST_MINT) invoice $$AMOUNT && \
193 echo "--- Token (cashuA legacy) ---" && \ 222 echo "--- Token (cashuA legacy) ---" && \
194 cashu --env-mint $(TEST_MINT) send --legacy $$AMOUNT 223 cashu --env-mint $(TEST_MINT) send --legacy $$AMOUNT
@@ -200,10 +229,6 @@ send-token:
200 229
201tokens: send-token 230tokens: send-token
202 231
203test-payment:
204 @echo "=== Running payment tests ==="
205 $(NODE) tests/phase2.mjs
206
207# ────────────────────────────────────────────── 232# ──────────────────────────────────────────────
208# Utilities 233# Utilities
209# ────────────────────────────────────────────── 234# ──────────────────────────────────────────────
diff --git a/package.json b/package.json
index dd61cd9..fe1daee 100644
--- a/package.json
+++ b/package.json
@@ -3,14 +3,18 @@
3 "version": "1.0.0", 3 "version": "1.0.0",
4 "private": true, 4 "private": true,
5 "scripts": { 5 "scripts": {
6 "test": "node tests/api.mjs && node tests/network.mjs", 6 "test": "node tests/integration/api.mjs && node tests/integration/network.mjs",
7 "test:api": "node tests/api.mjs", 7 "test:api": "node tests/integration/api.mjs",
8 "test:network": "node tests/network.mjs", 8 "test:network": "node tests/integration/network.mjs",
9 "test:portal": "npx playwright test tests/captive-portal.spec.mjs", 9 "test:smoke": "node tests/integration/smoke.mjs",
10 "test:happy-path": "npx playwright test tests/interop-happy-path.spec.mjs", 10 "test:payment": "node tests/integration/phase2.mjs",
11 "test:interop": "npx playwright test tests/interop-esp32-openwrt.spec.mjs", 11 "test:reset-auth": "node tests/integration/test-reset-auth.mjs",
12 "test:smoke": "node tests/smoke.mjs", 12 "test:session-expiry": "node tests/integration/test-session-expiry.mjs",
13 "test:playwright": "npx playwright test" 13 "test:dns-firewall": "node tests/integration/test-dns-firewall.mjs",
14 "test:portal": "npx playwright test -c tests/e2e/playwright.config.mjs captive-portal.spec.mjs",
15 "test:happy-path": "npx playwright test -c tests/e2e/playwright.config.mjs interop-happy-path.spec.mjs",
16 "test:e2e": "npx playwright test -c tests/e2e/playwright.config.mjs",
17 "test:playwright": "npx playwright test -c tests/e2e/playwright.config.mjs"
14 }, 18 },
15 "devDependencies": { 19 "devDependencies": {
16 "@playwright/test": "^1.52.0" 20 "@playwright/test": "^1.52.0"
diff --git a/tests/captive-portal.spec.mjs b/tests/e2e/captive-portal.spec.mjs
index 9411183..ab9d4f1 100644
--- a/tests/captive-portal.spec.mjs
+++ b/tests/e2e/captive-portal.spec.mjs
@@ -1,6 +1,6 @@
1import { test, expect } from '@playwright/test'; 1import { test, expect } from '@playwright/test';
2 2
3const PORTAL_IP = process.env.TOLLGATE_IP || '192.168.4.1'; 3const PORTAL_IP = process.env.TOLLGATE_IP || '10.192.45.1';
4const PORTAL_URL = `http://${PORTAL_IP}`; 4const PORTAL_URL = `http://${PORTAL_IP}`;
5const API_URL = `http://${PORTAL_IP}:2121`; 5const API_URL = `http://${PORTAL_IP}:2121`;
6 6
diff --git a/tests/interop-happy-path.spec.mjs b/tests/e2e/interop-happy-path.spec.mjs
index fe4fd78..fe4fd78 100644
--- a/tests/interop-happy-path.spec.mjs
+++ b/tests/e2e/interop-happy-path.spec.mjs
diff --git a/tests/playwright.config.mjs b/tests/e2e/playwright.config.mjs
index d4118b8..f4cbe01 100644
--- a/tests/playwright.config.mjs
+++ b/tests/e2e/playwright.config.mjs
@@ -9,7 +9,7 @@ export default defineConfig({
9 headless: true, 9 headless: true,
10 viewport: { width: 1280, height: 900 }, 10 viewport: { width: 1280, height: 900 },
11 screenshot: 'on', 11 screenshot: 'on',
12 video: 'on', 12 video: 'retain-on-failure',
13 trace: 'on-first-retry', 13 trace: 'on-first-retry',
14 }, 14 },
15 reporter: [['list'], ['html', { open: 'never' }]], 15 reporter: [['list'], ['html', { open: 'never' }]],
diff --git a/tests/helpers/network.mjs b/tests/helpers/network.mjs
index e4d5086..a2d889e 100644
--- a/tests/helpers/network.mjs
+++ b/tests/helpers/network.mjs
@@ -1,6 +1,6 @@
1import { execSync } from 'child_process'; 1import { execSync } from 'child_process';
2 2
3const ESP32_IP = process.env.TOLLGATE_IP || '192.168.4.1'; 3const ESP32_IP = process.env.TOLLGATE_IP || '10.192.45.1';
4const TIMEOUT = 5000; 4const TIMEOUT = 5000;
5 5
6export function curl(args, expectStatus = null) { 6export function curl(args, expectStatus = null) {
diff --git a/tests/api.mjs b/tests/integration/api.mjs
index 5218d7b..5218d7b 100644
--- a/tests/api.mjs
+++ b/tests/integration/api.mjs
diff --git a/tests/network.mjs b/tests/integration/network.mjs
index 2d302ef..dcd7a9a 100644
--- a/tests/network.mjs
+++ b/tests/integration/network.mjs
@@ -1,6 +1,6 @@
1import { execSync } from 'child_process'; 1import { execSync } from 'child_process';
2 2
3const IP = process.env.TOLLGATE_IP || '192.168.4.1'; 3const IP = process.env.TOLLGATE_IP || '10.192.45.1';
4let passed = 0, failed = 0; 4let passed = 0, failed = 0;
5 5
6function assert(condition, test) { 6function assert(condition, test) {
diff --git a/tests/phase2.mjs b/tests/integration/phase2.mjs
index 91891e7..9eaa7d7 100644
--- a/tests/phase2.mjs
+++ b/tests/integration/phase2.mjs
@@ -1,6 +1,6 @@
1import { execSync } from 'child_process'; 1import { execSync } from 'child_process';
2 2
3const IP = process.env.TOLLGATE_IP || '192.168.4.1'; 3const IP = process.env.TOLLGATE_IP || '10.192.45.1';
4const API = `http://${IP}:2121`; 4const API = `http://${IP}:2121`;
5let passed = 0, failed = 0; 5let passed = 0, failed = 0;
6 6
diff --git a/tests/smoke.mjs b/tests/integration/smoke.mjs
index 19f96de..f89eeac 100644
--- a/tests/smoke.mjs
+++ b/tests/integration/smoke.mjs
@@ -1,7 +1,7 @@
1import { execSync } from 'child_process'; 1import { execSync } from 'child_process';
2 2
3const PORT = process.argv[2] || '/dev/ttyACM0'; 3const PORT = process.argv[2] || '/dev/ttyACM0';
4const IP = process.env.TOLLGATE_IP || '192.168.4.1'; 4const IP = process.env.TOLLGATE_IP || '10.192.45.1';
5const SSID = process.env.AP_SSID || 'TollGate'; 5const SSID = process.env.AP_SSID || 'TollGate';
6 6
7console.log(`\n=== Smoke Test (30s) ===`); 7console.log(`\n=== Smoke Test (30s) ===`);
diff --git a/tests/integration/test-dns-firewall.mjs b/tests/integration/test-dns-firewall.mjs
new file mode 100644
index 0000000..b69b524
--- /dev/null
+++ b/tests/integration/test-dns-firewall.mjs
@@ -0,0 +1,123 @@
1import { execSync } from 'child_process';
2
3const IP = process.env.TOLLGATE_IP || '10.192.45.1';
4const API = `http://${IP}:2121`;
5let passed = 0, failed = 0;
6
7function assert(cond, msg) {
8 if (cond) { console.log(` ✓ ${msg}`); passed++; }
9 else { console.log(` ✗ ${msg}`); failed++; }
10}
11
12function run(cmd) {
13 try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); }
14 catch { return null; }
15}
16
17function runJson(cmd) {
18 const out = run(cmd);
19 try { return out ? JSON.parse(out) : null; }
20 catch { return null; }
21}
22
23function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
24
25function mintToken(amount = 21) {
26 run('cashu -h https://testnut.cashu.space invoice ' + amount + ' 2>&1');
27 const out = run('cashu -h https://testnut.cashu.space send --legacy ' + amount + ' 2>&1');
28 const match = out && out.match(/cashuA[a-zA-Z0-9_-]+/);
29 return match ? match[0] : null;
30}
31
32function dnsResolves(domain, server) {
33 const result = run(`nslookup -timeout=3 ${domain} ${server} 2>&1`);
34 return result && result.includes('Address') && !result.includes('NXDOMAIN');
35}
36
37function dnsResolvesToSelf(domain) {
38 try {
39 const result = run(`nslookup ${domain} ${IP} 2>&1`);
40 return result && result.includes(IP);
41 } catch {
42 return false;
43 }
44}
45
46function canPing(host = '8.8.8.8') {
47 const result = run(`ping -c 1 -W 2 -I wlp59s0 ${host} 2>/dev/null`);
48 return result && !result.includes('100% packet loss');
49}
50
51console.log(`\n=== DNS + Firewall Integration Test (target: ${IP}) ===\n`);
52
53console.log('--- Part 1: Before Authentication ---\n');
54
55console.log('1. DNS hijack: resolves to ESP32 AP IP');
56assert(dnsResolvesToSelf('google.com'), 'google.com resolves to AP IP');
57assert(dnsResolvesToSelf('random-test.example.com'), 'random domain resolves to AP IP');
58
59console.log('\n2. DNS hijack: upstream DNS not reachable');
60const upstreamResolve = run(`nslookup -timeout=3 google.com 8.8.8.8 2>&1`);
61assert(!upstreamResolve || upstreamResolve.includes('connection timed out') || upstreamResolve.includes('no servers'), 'Upstream DNS unreachable before auth');
62
63console.log('\n3. Per-client NAT filter: ping blocked');
64assert(!canPing(), 'Ping to 8.8.8.8 blocked by NAT filter');
65
66console.log('\n4. Per-client NAT filter: HTTP blocked');
67const httpBefore = run(`curl -s --connect-timeout 5 -m 5 --interface wlp59s0 http://1.1.1.1/ 2>/dev/null`);
68assert(!httpBefore || httpBefore.length === 0, 'HTTP blocked before auth');
69
70console.log('\n5. Captive portal and API still accessible');
71const portal = run(`curl -s --connect-timeout 5 http://${IP}/`);
72assert(portal && portal.includes('TollGate'), 'Portal HTML accessible');
73const apiDisc = runJson(`curl -s --connect-timeout 5 ${API}/`);
74assert(apiDisc && apiDisc.kind === 10021, 'API discovery accessible');
75
76console.log('\n--- Part 2: After Authentication ---\n');
77
78console.log('6. Reset + Pay');
79run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`);
80await sleep(1000);
81
82const token = mintToken(21);
83assert(token !== null, 'Token generated');
84if (token) {
85 const payResult = runJson(`curl -s --connect-timeout 20 -X POST --data-binary '${token}' -H "Content-Type: application/cashu" ${API}/`);
86 assert(payResult && payResult.kind === 1022, 'Payment accepted');
87}
88
89await sleep(1000);
90
91console.log('\n7. DNS now forwards to upstream');
92assert(dnsResolveWorks('google.com'), 'DNS resolves to real IPs after auth');
93
94console.log('\n8. Per-client NAT filter: ping allowed');
95assert(canPing(), 'Ping to 8.8.8.8 allowed after auth');
96
97console.log('\n9. Per-client NAT filter: HTTP allowed');
98const httpAfter = run(`curl -s --connect-timeout 10 -m 10 --interface wlp59s0 http://1.1.1.1/ 2>/dev/null`);
99assert(httpAfter && httpAfter.length > 0, 'HTTP allowed after auth');
100
101console.log('\n--- Part 3: After Revocation ---\n');
102
103console.log('10. Reset auth');
104run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`);
105await sleep(1000);
106
107console.log('\n11. DNS goes back to hijack');
108assert(dnsResolvesToSelf('google.com'), 'DNS hijack restored after revoke');
109
110console.log('\n12. Per-client NAT filter: ping blocked again');
111assert(!canPing(), 'Ping blocked after revoke');
112
113console.log('\n13. Per-client NAT filter: HTTP blocked again');
114const httpRevoke = run(`curl -s --connect-timeout 5 -m 5 --interface wlp59s0 http://1.1.1.1/ 2>/dev/null`);
115assert(!httpRevoke || httpRevoke.length === 0, 'HTTP blocked after revoke');
116
117function dnsResolveWorks(domain) {
118 const result = run(`nslookup -timeout=3 ${domain} 2>&1`);
119 return result && result.includes('Address') && !result.includes(IP) && !result.includes('NXDOMAIN');
120}
121
122console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`);
123process.exit(failed > 0 ? 1 : 0);
diff --git a/tests/integration/test-reset-auth.mjs b/tests/integration/test-reset-auth.mjs
new file mode 100644
index 0000000..279b2f9
--- /dev/null
+++ b/tests/integration/test-reset-auth.mjs
@@ -0,0 +1,101 @@
1import { execSync } from 'child_process';
2
3const IP = process.env.TOLLGATE_IP || '10.192.45.1';
4const API = `http://${IP}:2121`;
5const SUDO_PW = process.env.SUDO_PW || 'c03rad0r123';
6let passed = 0, failed = 0;
7
8function assert(cond, msg) {
9 if (cond) { console.log(` ✓ ${msg}`); passed++; }
10 else { console.log(` ✗ ${msg}`); failed++; }
11}
12
13function run(cmd) {
14 try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); }
15 catch { return null; }
16}
17
18function runJson(cmd) {
19 const out = run(cmd);
20 try { return out ? JSON.parse(out) : null; }
21 catch { return null; }
22}
23
24function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
25
26function mintToken(amount = 21) {
27 run('cashu -h https://testnut.cashu.space invoice ' + amount + ' 2>&1');
28 const out = run('cashu -h https://testnut.cashu.space send --legacy ' + amount + ' 2>&1');
29 const match = out && out.match(/cashuA[a-zA-Z0-9_-]+/);
30 return match ? match[0] : null;
31}
32
33function canPing(host = '8.8.8.8') {
34 const result = run(`ping -c 1 -W 2 -I wlp59s0 ${host} 2>/dev/null`);
35 return result && !result.includes('100% packet loss');
36}
37
38console.log(`\n=== Reset Auth Integration Test (target: ${IP}) ===\n`);
39
40console.log('1. Reset auth to clear state');
41const reset1 = run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`);
42assert(reset1 && reset1.includes('reset'), 'Reset returns {"status":"reset"}');
43
44await sleep(1000);
45
46console.log('\n2. Verify no session');
47const usage1 = run(`curl -s --connect-timeout 10 ${API}/usage`);
48assert(usage1 && usage1.includes('-1/-1'), 'Usage is -1/-1 before payment');
49
50console.log('\n3. Verify internet blocked');
51assert(!canPing(), 'Ping blocked before payment');
52
53console.log('\n4. Pay with valid token');
54const token = mintToken(21);
55assert(token !== null, 'Token generated');
56if (token) {
57 const payResult = runJson(`curl -s --connect-timeout 20 -X POST --data-binary '${token}' -H "Content-Type: application/cashu" ${API}/`);
58 assert(payResult && payResult.kind === 1022, 'Payment accepted (kind=1022)');
59 const allotment = payResult && payResult.tags && payResult.tags.find(t => t[0] === 'allotment');
60 assert(allotment && parseInt(allotment[1]) > 0, `Allotment: ${allotment ? allotment[1] : 'N/A'}ms`);
61}
62
63await sleep(1000);
64
65console.log('\n5. Verify session active');
66const usage2 = run(`curl -s --connect-timeout 10 ${API}/usage`);
67assert(usage2 && !usage2.includes('-1/-1'), `Usage: ${usage2}`);
68
69console.log('\n6. Verify internet allowed');
70assert(canPing(), 'Ping works with active session');
71
72console.log('\n7. Reset auth while session active');
73const reset2 = run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`);
74assert(reset2 && reset2.includes('reset'), 'Reset returns {"status":"reset"}');
75
76await sleep(1000);
77
78console.log('\n8. Verify session cleared');
79const usage3 = run(`curl -s --connect-timeout 10 ${API}/usage`);
80assert(usage3 && usage3.includes('-1/-1'), 'Usage is -1/-1 after reset');
81
82console.log('\n9. Verify internet blocked again');
83assert(!canPing(), 'Ping blocked after reset');
84
85console.log('\n10. Pay again (new token)');
86const token2 = mintToken(21);
87if (token2) {
88 const pay2 = runJson(`curl -s --connect-timeout 20 -X POST --data-binary '${token2}' -H "Content-Type: application/cashu" ${API}/`);
89 assert(pay2 && pay2.kind === 1022, 'Second payment accepted');
90}
91
92await sleep(1000);
93
94console.log('\n11. Verify internet works again');
95assert(canPing(), 'Ping works with new session');
96
97console.log('\n12. Final reset');
98run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`);
99
100console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`);
101process.exit(failed > 0 ? 1 : 0);
diff --git a/tests/integration/test-session-expiry.mjs b/tests/integration/test-session-expiry.mjs
new file mode 100644
index 0000000..c8334ab
--- /dev/null
+++ b/tests/integration/test-session-expiry.mjs
@@ -0,0 +1,103 @@
1import { execSync } from 'child_process';
2
3const IP = process.env.TOLLGATE_IP || '10.192.45.1';
4const API = `http://${IP}:2121`;
5let passed = 0, failed = 0;
6
7function assert(cond, msg) {
8 if (cond) { console.log(` ✓ ${msg}`); passed++; }
9 else { console.log(` ✗ ${msg}`); failed++; }
10}
11
12function run(cmd) {
13 try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); }
14 catch { return null; }
15}
16
17function runJson(cmd) {
18 const out = run(cmd);
19 try { return out ? JSON.parse(out) : null; }
20 catch { return null; }
21}
22
23function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
24
25function mintToken(amount = 21) {
26 run('cashu -h https://testnut.cashu.space invoice ' + amount + ' 2>&1');
27 const out = run('cashu -h https://testnut.cashu.space send --legacy ' + amount + ' 2>&1');
28 const match = out && out.match(/cashuA[a-zA-Z0-9_-]+/);
29 return match ? match[0] : null;
30}
31
32function canPing(host = '8.8.8.8') {
33 const result = run(`ping -c 1 -W 2 -I wlp59s0 ${host} 2>/dev/null`);
34 return result && !result.includes('100% packet loss');
35}
36
37console.log(`\n=== Session Expiry Integration Test (target: ${IP}) ===`);
38console.log(`NOTE: This test waits 65s for session expiry. Total runtime ~80s.\n`);
39
40console.log('1. Reset auth');
41run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`);
42
43await sleep(1000);
44
45console.log('\n2. Verify blocked before payment');
46assert(!canPing(), 'Ping blocked before payment');
47
48const usage0 = run(`curl -s --connect-timeout 10 ${API}/usage`);
49assert(usage0 && usage0.includes('-1/-1'), 'Usage is -1/-1');
50
51console.log('\n3. Pay with valid token (21 sats = 60000ms)');
52const token = mintToken(21);
53assert(token !== null, 'Token generated');
54if (token) {
55 const payResult = runJson(`curl -s --connect-timeout 20 -X POST --data-binary '${token}' -H "Content-Type: application/cashu" ${API}/`);
56 assert(payResult && payResult.kind === 1022, 'Payment accepted');
57}
58
59await sleep(1000);
60
61console.log('\n4. Verify session active');
62const usage1 = run(`curl -s --connect-timeout 10 ${API}/usage`);
63assert(usage1 && !usage1.includes('-1/-1'), `Usage: ${usage1}`);
64
65console.log('\n5. Verify internet works');
66assert(canPing(), 'Ping works with active session');
67
68const httpResult = run(`curl -s --connect-timeout 10 -m 10 --interface wlp59s0 http://1.1.1.1/ 2>/dev/null`);
69assert(httpResult && httpResult.length > 0, 'HTTP request reaches internet');
70
71console.log('\n6. Waiting 65s for session expiry (allotment=60000ms)...');
72for (let i = 65; i > 0; i -= 5) {
73 process.stdout.write(`\r ${i}s remaining...`);
74 await sleep(Math.min(5000, i * 1000));
75}
76console.log('\r Session should be expired now. ');
77
78console.log('\n7. Verify session expired');
79const usage2 = run(`curl -s --connect-timeout 10 ${API}/usage`);
80assert(usage2 && usage2.includes('-1/-1'), `Usage after expiry: ${usage2}`);
81
82console.log('\n8. Verify internet blocked after expiry');
83assert(!canPing(), 'Ping blocked after session expiry');
84
85const httpResult2 = run(`curl -s --connect-timeout 5 -m 5 --interface wlp59s0 http://1.1.1.1/ 2>/dev/null`);
86assert(!httpResult2 || httpResult2.length === 0, 'HTTP blocked after expiry');
87
88console.log('\n9. Pay again to verify renewal works');
89const token2 = mintToken(21);
90if (token2) {
91 const pay2 = runJson(`curl -s --connect-timeout 20 -X POST --data-binary '${token2}' -H "Content-Type: application/cashu" ${API}/`);
92 assert(pay2 && pay2.kind === 1022, 'Renewal payment accepted');
93}
94
95await sleep(1000);
96
97console.log('\n10. Verify internet works after renewal');
98assert(canPing(), 'Ping works after renewal');
99
100run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`);
101
102console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`);
103process.exit(failed > 0 ? 1 : 0);