upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-15 22:27:14 +0530
committerYour Name <you@example.com>2026-05-15 22:27:14 +0530
commit1263d86314fc0760d9be8eea415ccecbc047a5eb (patch)
tree778130f0beb59d52f68e0e5f11388bf4b1470130 /tests
parenta7d0a672d59bf8985a6fc0e61b49015fabd96513 (diff)
Phase 2 WIP: Cashu payment endpoints, session tracking, updated checklist
- Add cashu.c/h: Cashu token decode (cashuA/base64url), proof state check via mint API, allotment calculator - Add session.c/h: time-based session management with allotment/expiry, spent secret tracking - Add tollgate_api.c/h: HTTP server on :2121 with GET / (kind=10021 discovery), POST / (payment processing), /usage, /whoami - Update captive portal HTML: replace Grant Free Access with Cashu token paste form + Pay & Connect button - Update tollgate_main.c: wire in session manager, TollGate API, 1s session tick loop - Add tests/phase2.mjs: Phase 2 test suite (discovery, invalid token, wrong mint, valid payment) - Update CHECKLIST.md: reflect Phase 1 complete, Phase 2 in progress with known bugs Known issues (not yet flashed): - Stack overflow crash in httpd POST handler (need stack_size=16384 + heap allocations) - cashu_decode_token uses 2KB stack buffer (needs heap alloc) - Mint URL should be testnut.cashu.space (nofee.testnut has API compat issues)
Diffstat (limited to 'tests')
-rw-r--r--tests/phase2.mjs101
1 files changed, 101 insertions, 0 deletions
diff --git a/tests/phase2.mjs b/tests/phase2.mjs
new file mode 100644
index 0000000..3136da3
--- /dev/null
+++ b/tests/phase2.mjs
@@ -0,0 +1,101 @@
1import { execSync } from 'child_process';
2
3const IP = process.env.TOLLGATE_IP || '192.168.4.1';
4const API = `http://${IP}:2121`;
5let passed = 0, failed = 0;
6
7function assert(condition, test) {
8 if (condition) { console.log(` ✓ ${test}`); passed++; }
9 else { console.log(` ✗ ${test}`); failed++; }
10}
11
12function curlBody(url, options = {}) {
13 const cmd = options.method
14 ? `curl -s --connect-timeout 5 --max-time 10 -X ${options.method} ${options.data ? `-d '${options.data.replace(/'/g, "'\\''")}'` : ''} "${url}"`
15 : `curl -s --connect-timeout 5 --max-time 10 "${url}"`;
16 try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); }
17 catch { return null; }
18}
19
20function curlStatus(url, options = {}) {
21 const cmd = `curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 --max-time 10 ${options.method ? `-X ${options.method}` : ''} ${options.data ? `-d '${options.data.replace(/'/g, "'\\''")}'` : ''} "${url}"`;
22 try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }).trim(); }
23 catch { return null; }
24}
25
26async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
27
28console.log(`\n=== Phase 2 Tests (target: ${API}) ===\n`);
29
30// Test 15: Advertisement valid
31console.log('Test 15: GET :2121/ returns kind=10021 advertisement');
32const body15 = curlBody(`${API}/`);
33const json15 = body15 ? JSON.parse(body15) : null;
34assert(json15 && json15.kind === 10021, 'kind=10021');
35assert(json15 && json15.tags && json15.tags.some(t => t[0] === 'price_per_step'), 'Has price_per_step tag');
36assert(json15 && json15.tags && json15.tags.some(t => t[0] === 'step_size'), 'Has step_size tag');
37assert(json15 && json15.tags && json15.tags.some(t => t[0] === 'metric'), 'Has metric tag');
38
39// Test 19: Invalid token
40console.log('\nTest 19: POST :2121/ with invalid token');
41const body19 = curlBody(`${API}/`, { method: 'POST', data: 'garbage_not_a_token' });
42const json19 = body19 ? JSON.parse(body19) : null;
43assert(json19 && json19.kind === 21023, 'Returns kind=21023 notice');
44assert(json19 && json19.tags && json19.tags.some(t => t[0] === 'code'), 'Has error code tag');
45const status19 = curlStatus(`${API}/`, { method: 'POST', data: 'garbage_not_a_token' });
46assert(status19 === '400', 'Returns HTTP 400');
47
48// Test 21: Wrong mint (token from wrong mint)
49console.log('\nTest 21: POST :2121/ with wrong mint token');
50const wrongMintToken = 'cashuA' + Buffer.from(JSON.stringify({
51 token: [{ mint: 'https://wrong.mint.example.com', proofs: [{ amount: 21, secret: 'test', id: '00'.repeat(8), C: '02'.repeat(33) }] }]
52})).toString('base64url');
53const body21 = curlBody(`${API}/`, { method: 'POST', data: wrongMintToken });
54const json21 = body21 ? JSON.parse(body21) : null;
55assert(json21 && json21.kind === 21023, 'Returns kind=21023');
56const codeTag21 = json21 && json21.tags && json21.tags.find(t => t[0] === 'code');
57assert(codeTag21 && codeTag21[1] === 'payment-error-mint-not-accepted', 'Error code: mint-not-accepted');
58
59// Test valid token (if provided)
60const TEST_TOKEN = process.env.TEST_TOKEN;
61if (TEST_TOKEN) {
62 console.log('\nTest 16: POST :2121/ with valid token');
63 const body16 = curlBody(`${API}/`, { method: 'POST', data: TEST_TOKEN });
64 const json16 = body16 ? JSON.parse(body16) : null;
65 assert(json16 && json16.kind === 1022, 'Returns kind=1022 session');
66 assert(json16 && json16.tags && json16.tags.some(t => t[0] === 'allotment'), 'Has allotment tag');
67
68 // Test 17: Usage tracking
69 console.log('\nTest 17: GET :2121/usage after payment');
70 const body17 = curlBody(`${API}/usage`);
71 assert(body17 && !body17.includes('-1/-1'), 'Returns active usage');
72
73 // Test 18: Internet after payment
74 console.log('\nTest 18: Internet works after payment');
75 await sleep(1000);
76 const ping18 = execSync('ping -c 2 -W 2 -I wlp59s0 8.8.8.8', { encoding: 'utf8', timeout: 10000 });
77 assert(ping18 && !ping18.includes('100% packet loss'), 'Internet works');
78
79 // Test 20: Spent token
80 console.log('\nTest 20: Reuse token (should fail)');
81 const body20 = curlBody(`${API}/`, { method: 'POST', data: TEST_TOKEN });
82 const json20 = body20 ? JSON.parse(body20) : null;
83 assert(json20 && json20.kind === 21023, 'Returns kind=21023 for spent token');
84} else {
85 console.log('\n ⚠ Skipping tests 16-20: Set TEST_TOKEN env var with a valid Cashu token');
86}
87
88// Test: whoami on :2121
89console.log('\nTest: GET :2121/whoami');
90const bodyWhoami = curlBody(`${API}/whoami`);
91assert(bodyWhoami && bodyWhoami.startsWith('mac='), '/whoami returns mac=...');
92
93// Test: Portal has payment form
94console.log('\nTest: Portal has payment form');
95const bodyPortal = curlBody(`http://${IP}/`);
96assert(bodyPortal && bodyPortal.includes('cashuA'), 'Portal has Cashu token input');
97assert(bodyPortal && bodyPortal.includes('Pay &amp; Connect') || bodyPortal && bodyPortal.includes('Pay'), 'Portal has Pay button');
98
99// Summary
100console.log(`\n=== Phase 2 Results: ${passed} passed, ${failed} failed ===\n`);
101process.exit(failed > 0 ? 1 : 0);