From 1263d86314fc0760d9be8eea415ccecbc047a5eb Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 15 May 2026 22:27:14 +0530 Subject: 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) --- tests/phase2.mjs | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 tests/phase2.mjs (limited to 'tests/phase2.mjs') 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 @@ +import { execSync } from 'child_process'; + +const IP = process.env.TOLLGATE_IP || '192.168.4.1'; +const API = `http://${IP}:2121`; +let passed = 0, failed = 0; + +function assert(condition, test) { + if (condition) { console.log(` ✓ ${test}`); passed++; } + else { console.log(` ✗ ${test}`); failed++; } +} + +function curlBody(url, options = {}) { + const cmd = options.method + ? `curl -s --connect-timeout 5 --max-time 10 -X ${options.method} ${options.data ? `-d '${options.data.replace(/'/g, "'\\''")}'` : ''} "${url}"` + : `curl -s --connect-timeout 5 --max-time 10 "${url}"`; + try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); } + catch { return null; } +} + +function curlStatus(url, options = {}) { + 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}"`; + try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }).trim(); } + catch { return null; } +} + +async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } + +console.log(`\n=== Phase 2 Tests (target: ${API}) ===\n`); + +// Test 15: Advertisement valid +console.log('Test 15: GET :2121/ returns kind=10021 advertisement'); +const body15 = curlBody(`${API}/`); +const json15 = body15 ? JSON.parse(body15) : null; +assert(json15 && json15.kind === 10021, 'kind=10021'); +assert(json15 && json15.tags && json15.tags.some(t => t[0] === 'price_per_step'), 'Has price_per_step tag'); +assert(json15 && json15.tags && json15.tags.some(t => t[0] === 'step_size'), 'Has step_size tag'); +assert(json15 && json15.tags && json15.tags.some(t => t[0] === 'metric'), 'Has metric tag'); + +// Test 19: Invalid token +console.log('\nTest 19: POST :2121/ with invalid token'); +const body19 = curlBody(`${API}/`, { method: 'POST', data: 'garbage_not_a_token' }); +const json19 = body19 ? JSON.parse(body19) : null; +assert(json19 && json19.kind === 21023, 'Returns kind=21023 notice'); +assert(json19 && json19.tags && json19.tags.some(t => t[0] === 'code'), 'Has error code tag'); +const status19 = curlStatus(`${API}/`, { method: 'POST', data: 'garbage_not_a_token' }); +assert(status19 === '400', 'Returns HTTP 400'); + +// Test 21: Wrong mint (token from wrong mint) +console.log('\nTest 21: POST :2121/ with wrong mint token'); +const wrongMintToken = 'cashuA' + Buffer.from(JSON.stringify({ + token: [{ mint: 'https://wrong.mint.example.com', proofs: [{ amount: 21, secret: 'test', id: '00'.repeat(8), C: '02'.repeat(33) }] }] +})).toString('base64url'); +const body21 = curlBody(`${API}/`, { method: 'POST', data: wrongMintToken }); +const json21 = body21 ? JSON.parse(body21) : null; +assert(json21 && json21.kind === 21023, 'Returns kind=21023'); +const codeTag21 = json21 && json21.tags && json21.tags.find(t => t[0] === 'code'); +assert(codeTag21 && codeTag21[1] === 'payment-error-mint-not-accepted', 'Error code: mint-not-accepted'); + +// Test valid token (if provided) +const TEST_TOKEN = process.env.TEST_TOKEN; +if (TEST_TOKEN) { + console.log('\nTest 16: POST :2121/ with valid token'); + const body16 = curlBody(`${API}/`, { method: 'POST', data: TEST_TOKEN }); + const json16 = body16 ? JSON.parse(body16) : null; + assert(json16 && json16.kind === 1022, 'Returns kind=1022 session'); + assert(json16 && json16.tags && json16.tags.some(t => t[0] === 'allotment'), 'Has allotment tag'); + + // Test 17: Usage tracking + console.log('\nTest 17: GET :2121/usage after payment'); + const body17 = curlBody(`${API}/usage`); + assert(body17 && !body17.includes('-1/-1'), 'Returns active usage'); + + // Test 18: Internet after payment + console.log('\nTest 18: Internet works after payment'); + await sleep(1000); + const ping18 = execSync('ping -c 2 -W 2 -I wlp59s0 8.8.8.8', { encoding: 'utf8', timeout: 10000 }); + assert(ping18 && !ping18.includes('100% packet loss'), 'Internet works'); + + // Test 20: Spent token + console.log('\nTest 20: Reuse token (should fail)'); + const body20 = curlBody(`${API}/`, { method: 'POST', data: TEST_TOKEN }); + const json20 = body20 ? JSON.parse(body20) : null; + assert(json20 && json20.kind === 21023, 'Returns kind=21023 for spent token'); +} else { + console.log('\n ⚠ Skipping tests 16-20: Set TEST_TOKEN env var with a valid Cashu token'); +} + +// Test: whoami on :2121 +console.log('\nTest: GET :2121/whoami'); +const bodyWhoami = curlBody(`${API}/whoami`); +assert(bodyWhoami && bodyWhoami.startsWith('mac='), '/whoami returns mac=...'); + +// Test: Portal has payment form +console.log('\nTest: Portal has payment form'); +const bodyPortal = curlBody(`http://${IP}/`); +assert(bodyPortal && bodyPortal.includes('cashuA'), 'Portal has Cashu token input'); +assert(bodyPortal && bodyPortal.includes('Pay & Connect') || bodyPortal && bodyPortal.includes('Pay'), 'Portal has Pay button'); + +// Summary +console.log(`\n=== Phase 2 Results: ${passed} passed, ${failed} failed ===\n`); +process.exit(failed > 0 ? 1 : 0); -- cgit v1.2.3