From 8071741815f0b0938701e80a63e80b0ec94b2778 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 17 May 2026 17:18:43 +0530 Subject: refactor: reorganize test suite, add integration tests for NAT filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move integration tests (api, network, phase2, smoke) to tests/integration/ - Move Playwright specs (captive-portal, interop-happy-path) to tests/e2e/ - Move playwright.config.mjs to tests/e2e/ - Fix hardcoded IP fallbacks: 192.168.4.1 → 10.192.45.1 - Add test-reset-auth.mjs: reset→pay→allow→revoke→block cycle - Add test-session-expiry.mjs: pay→wait 65s→verify blocked (slow test) - Add test-dns-firewall.mjs: DNS hijack/forward + per-client NAT filter - Update Makefile with test-unit, test-integration, test-e2e, test-all targets - Update package.json scripts for new paths - Fix Playwright video: retain-on-failure instead of always-on - Update AGENTS.md: per-client NAT filter description - Update CHECKLIST.md: mark completed items, add Board B identity - Board B nsec: 9af47906... → SSID TollGate-b96d80, AP IP 10.185.47.1 - 186 unit tests passing --- tests/integration/api.mjs | 79 ++++++++++++++++ tests/integration/network.mjs | 66 +++++++++++++ tests/integration/phase2.mjs | 151 ++++++++++++++++++++++++++++++ tests/integration/smoke.mjs | 52 ++++++++++ tests/integration/test-dns-firewall.mjs | 123 ++++++++++++++++++++++++ tests/integration/test-reset-auth.mjs | 101 ++++++++++++++++++++ tests/integration/test-session-expiry.mjs | 103 ++++++++++++++++++++ 7 files changed, 675 insertions(+) create mode 100644 tests/integration/api.mjs create mode 100644 tests/integration/network.mjs create mode 100644 tests/integration/phase2.mjs create mode 100644 tests/integration/smoke.mjs create mode 100644 tests/integration/test-dns-firewall.mjs create mode 100644 tests/integration/test-reset-auth.mjs create mode 100644 tests/integration/test-session-expiry.mjs (limited to 'tests/integration') diff --git a/tests/integration/api.mjs b/tests/integration/api.mjs new file mode 100644 index 0000000..5218d7b --- /dev/null +++ b/tests/integration/api.mjs @@ -0,0 +1,79 @@ +import { curl, curlBody, getPortalIP, canPing, canResolve, dnsResolvesToSelf } from './helpers/network.mjs'; + +const IP = getPortalIP(); +let passed = 0, failed = 0; + +function assert(condition, test) { + if (condition) { console.log(` ✓ ${test}`); passed++; } + else { console.log(` ✗ ${test}`); failed++; } +} + +async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } + +console.log(`\n=== API Tests (target: ${IP}) ===\n`); + +// Test 3: Captive portal serves HTML +console.log('Test 3: GET / returns portal HTML'); +const body3 = curlBody(`http://${IP}/`); +assert(body3 && body3.includes('TollGate'), 'Portal HTML contains "TollGate"'); +assert(body3 && body3.includes('Grant Free Access'), 'Portal has Grant Access button'); + +// Test 4: Captive detection URIs +console.log('\nTest 4: Captive detection URIs'); +for (const uri of ['/generate_204', '/hotspot-detect.html', '/canonical.html', '/success.txt', '/ncsi.txt', '/connecttest.txt', '/wpad.dat', '/redirect']) { + const code = curl(`http://${IP}${uri}`); + assert(code === '200', `${uri} → 200`); +} + +// Test 7: /whoami returns MAC +console.log('\nTest 7: GET /whoami'); +const body7 = curlBody(`http://${IP}/whoami`); +assert(body7 && body7.startsWith('mac='), '/whoami returns mac=...'); + +// Test 8: /usage returns no session +console.log('\nTest 8: GET /usage'); +const body8 = curlBody(`http://${IP}/usage`); +assert(body8 && body8.includes('-1/-1'), '/usage returns -1/-1 before auth'); + +// Test 5: DNS hijack before auth +console.log('\nTest 5: DNS hijack before auth'); +assert(dnsResolvesToSelf('google.com'), 'DNS resolves google.com to AP IP'); + +// Test 6: No internet before auth +console.log('\nTest 6: No internet before auth'); +assert(!canPing('8.8.8.8', 1), 'ping 8.8.8.8 fails before auth'); + +// Test 9: Grant access +console.log('\nTest 9: GET /grant_access'); +const body9 = curlBody(`http://${IP}/grant_access`); +assert(body9 && body9.includes('"granted"'), 'Grant access returns {"status":"granted"}'); + +await sleep(2000); + +// Test 10: DNS forward after auth +console.log('\nTest 10: DNS forward after auth'); +assert(canResolve('google.com'), 'DNS resolves normally after auth'); + +// Test 11: Internet after auth +console.log('\nTest 11: Internet after auth'); +assert(canPing('8.8.8.8'), 'ping 8.8.8.8 succeeds after auth'); + +// Test 12: HTTP browsing works +console.log('\nTest 12: HTTP browsing'); +const body12 = curlBody('http://example.com/'); +assert(body12 && (body12.includes('Example Domain') || body12.includes('example')), 'HTTP page loads'); + +// Test 13: Reset auth +console.log('\nTest 13: GET /reset_authentication'); +const body13 = curlBody(`http://${IP}/reset_authentication`); +assert(body13 && body13.includes('"reset"'), 'Reset returns {"status":"reset"}'); + +await sleep(2000); + +// Test 14: Internet blocked after reset +console.log('\nTest 14: Internet blocked after reset'); +assert(!canPing('8.8.8.8', 1), 'ping fails after auth reset'); + +// Summary +console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`); +process.exit(failed > 0 ? 1 : 0); diff --git a/tests/integration/network.mjs b/tests/integration/network.mjs new file mode 100644 index 0000000..dcd7a9a --- /dev/null +++ b/tests/integration/network.mjs @@ -0,0 +1,66 @@ +import { execSync } from 'child_process'; + +const IP = process.env.TOLLGATE_IP || '10.192.45.1'; +let passed = 0, failed = 0; + +function assert(condition, test) { + if (condition) { console.log(` ✓ ${test}`); passed++; } + else { console.log(` ✗ ${test}`); failed++; } +} + +function run(cmd) { + try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); } + catch { return null; } +} + +console.log(`\n=== Network Tests (target: ${IP}) ===\n`); + +// Test 1: AP visible in scan +console.log('Test 1: AP visible in scan'); +const scan = run('nmcli -t -f SSID dev wifi list 2>/dev/null'); +assert(scan && scan.includes('TollGate'), 'TollGate SSID visible in WiFi scan'); + +// Test 2: DHCP lease +console.log('\nTest 2: DHCP lease / connectivity'); +const ip_show = run(`ip addr show | grep "inet ${IP.split('.').slice(0,3).join('.')}"`); +assert(ip_show !== null, `Has IP in ${IP.split('.').slice(0,3).join('.')}.* subnet`); + +// Test 5: DNS hijack +console.log('\nTest 5: DNS hijack before auth'); +const ns1 = run(`nslookup random-test.example.com ${IP} 2>/dev/null`); +assert(ns1 && ns1.includes(IP), 'DNS resolves arbitrary domain to AP IP'); + +// Test 6: No internet +console.log('\nTest 6: No internet before auth'); +const ping1 = run('ping -c 1 -W 3 1.1.1.1 2>/dev/null'); +assert(ping1 === null || ping1.includes('100% packet loss'), 'Internet blocked before auth'); + +// Grant access for further tests +console.log('\nGranting access...'); +run(`curl -s http://${IP}/grant_access`); + +import { execSync as exec } from 'child_process'; +await new Promise(r => setTimeout(r, 2000)); + +// Test 10: DNS forward +console.log('Test 10: DNS forward after auth'); +const ns2 = run(`nslookup google.com ${IP} 2>/dev/null`); +assert(ns2 && !ns2.includes(IP) && ns2.includes('Address'), 'DNS resolves to real IPs'); + +// Test 11: Internet +console.log('\nTest 11: Internet after auth'); +const ping2 = run('ping -c 2 -W 3 8.8.8.8'); +assert(ping2 && !ping2.includes('100% packet loss'), 'ping succeeds after auth'); + +// Reset +console.log('\nResetting auth...'); +run(`curl -s http://${IP}/reset_authentication`); +await new Promise(r => setTimeout(r, 2000)); + +// Test 14 +console.log('Test 14: Internet blocked after reset'); +const ping3 = run('ping -c 1 -W 3 8.8.8.8 2>/dev/null'); +assert(ping3 === null || ping3.includes('100% packet loss'), 'Internet blocked after reset'); + +console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`); +process.exit(failed > 0 ? 1 : 0); diff --git a/tests/integration/phase2.mjs b/tests/integration/phase2.mjs new file mode 100644 index 0000000..9eaa7d7 --- /dev/null +++ b/tests/integration/phase2.mjs @@ -0,0 +1,151 @@ +import { execSync } from 'child_process'; + +const IP = process.env.TOLLGATE_IP || '10.192.45.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(1500); + const sudoPw = process.env.SUDO_PW || 'c03rad0r123'; + try { + execSync(`echo '${sudoPw}' | sudo -S ip route add default via ${IP} dev wlp59s0 metric 50 2>/dev/null`, { encoding: 'utf8', timeout: 5000 }); + } catch {} + let pingOk = false; + try { + const ping18 = execSync('ping -c 3 -W 3 8.8.8.8', { encoding: 'utf8', timeout: 15000 }); + pingOk = ping18 && !ping18.includes('100% packet loss'); + } catch { + pingOk = false; + } + assert(pingOk, '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'); + + // Test 22: Session expiry + console.log('\nTest 22: Session expiry (waiting 65s for allotment to expire)...'); + try { + execSync(`echo '${sudoPw}' | sudo -S ip route add default via ${IP} dev wlp59s0 metric 50 2>/dev/null`, { encoding: 'utf8', timeout: 5000 }); + } catch {} + await sleep(65000); + let expiredPingOk = true; + try { + const ping22 = execSync('ping -c 2 -W 2 8.8.8.8', { encoding: 'utf8', timeout: 10000 }); + expiredPingOk = !ping22.includes('100% packet loss'); + } catch { + expiredPingOk = false; + } + assert(!expiredPingOk, 'Internet blocked after session expiry'); + const body22 = curlBody(`${API}/usage`); + assert(body22 && body22.includes('-1/-1'), 'Usage returns -1/-1 after expiry'); + + // Test 23: Session renewal + const TEST_TOKEN2 = process.env.TEST_TOKEN2; + if (TEST_TOKEN2) { + console.log('\nTest 23: Session renewal with second token'); + const body23 = curlBody(`${API}/`, { method: 'POST', data: TEST_TOKEN2 }); + const json23 = body23 ? JSON.parse(body23) : null; + assert(json23 && json23.kind === 1022, 'Returns kind=1022 for renewal'); + await sleep(1500); + let renewPingOk = false; + try { + const ping23 = execSync('ping -c 2 -W 2 8.8.8.8', { encoding: 'utf8', timeout: 10000 }); + renewPingOk = !ping23.includes('100% packet loss'); + } catch { + renewPingOk = false; + } + assert(renewPingOk, 'Internet works after renewal'); + } else { + console.log('\n ⚠ Skipping test 23: Set TEST_TOKEN2 env var for renewal test'); + } + try { + execSync(`echo '${sudoPw}' | sudo -S ip route del default via ${IP} dev wlp59s0 metric 50 2>/dev/null`, { encoding: 'utf8', timeout: 5000 }); + } catch {} +} 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.includes('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); diff --git a/tests/integration/smoke.mjs b/tests/integration/smoke.mjs new file mode 100644 index 0000000..f89eeac --- /dev/null +++ b/tests/integration/smoke.mjs @@ -0,0 +1,52 @@ +import { execSync } from 'child_process'; + +const PORT = process.argv[2] || '/dev/ttyACM0'; +const IP = process.env.TOLLGATE_IP || '10.192.45.1'; +const SSID = process.env.AP_SSID || 'TollGate'; + +console.log(`\n=== Smoke Test (30s) ===`); +console.log(`Port: ${PORT}, Portal IP: ${IP}, SSID: ${SSID}\n`); + +let passed = 0, failed = 0; +function assert(cond, msg) { + if (cond) { console.log(` ✓ ${msg}`); passed++; } + else { console.log(` ✗ ${msg}`); failed++; } +} + +function run(cmd) { + try { return execSync(cmd, { encoding: 'utf8', timeout: 10000 }); } + catch { return null; } +} + +// 1. Check AP visible +const scan = run('nmcli -t -f SSID dev wifi list 2>/dev/null'); +assert(scan && scan.includes(SSID), `SSID "${SSID}" visible`); + +// 2. Check we can reach portal +const portal = run(`curl -s --connect-timeout 5 http://${IP}/`); +assert(portal && portal.includes('TollGate'), 'Portal HTML loads'); + +// 3. Grant access +const grant = run(`curl -s http://${IP}/grant_access`); +assert(grant && grant.includes('granted'), 'Grant access works'); + +// Wait for DNS +const sleep = ms => new Promise(r => setTimeout(r, ms)); +await sleep(2000); + +// 4. Internet works +const ping = run('ping -c 1 -W 3 -I wlp59s0 1.1.1.1 2>/dev/null'); +assert(ping && !ping.includes('100% packet loss'), 'Internet works after grant'); + +// 5. Reset +const reset = run(`curl -s http://${IP}/reset_authentication`); +assert(reset && reset.includes('reset'), 'Reset auth works'); + +await sleep(2000); + +// 6. Internet blocked +const ping2 = run('ping -c 1 -W 3 -I wlp59s0 1.1.1.1 2>/dev/null'); +assert(!ping2 || ping2.includes('100% packet loss'), 'Internet blocked after reset'); + +console.log(`\n=== Smoke: ${passed} passed, ${failed} failed ===\n`); +process.exit(failed > 0 ? 1 : 0); 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 @@ +import { execSync } from 'child_process'; + +const IP = process.env.TOLLGATE_IP || '10.192.45.1'; +const API = `http://${IP}:2121`; +let passed = 0, failed = 0; + +function assert(cond, msg) { + if (cond) { console.log(` ✓ ${msg}`); passed++; } + else { console.log(` ✗ ${msg}`); failed++; } +} + +function run(cmd) { + try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); } + catch { return null; } +} + +function runJson(cmd) { + const out = run(cmd); + try { return out ? JSON.parse(out) : null; } + catch { return null; } +} + +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } + +function mintToken(amount = 21) { + run('cashu -h https://testnut.cashu.space invoice ' + amount + ' 2>&1'); + const out = run('cashu -h https://testnut.cashu.space send --legacy ' + amount + ' 2>&1'); + const match = out && out.match(/cashuA[a-zA-Z0-9_-]+/); + return match ? match[0] : null; +} + +function dnsResolves(domain, server) { + const result = run(`nslookup -timeout=3 ${domain} ${server} 2>&1`); + return result && result.includes('Address') && !result.includes('NXDOMAIN'); +} + +function dnsResolvesToSelf(domain) { + try { + const result = run(`nslookup ${domain} ${IP} 2>&1`); + return result && result.includes(IP); + } catch { + return false; + } +} + +function canPing(host = '8.8.8.8') { + const result = run(`ping -c 1 -W 2 -I wlp59s0 ${host} 2>/dev/null`); + return result && !result.includes('100% packet loss'); +} + +console.log(`\n=== DNS + Firewall Integration Test (target: ${IP}) ===\n`); + +console.log('--- Part 1: Before Authentication ---\n'); + +console.log('1. DNS hijack: resolves to ESP32 AP IP'); +assert(dnsResolvesToSelf('google.com'), 'google.com resolves to AP IP'); +assert(dnsResolvesToSelf('random-test.example.com'), 'random domain resolves to AP IP'); + +console.log('\n2. DNS hijack: upstream DNS not reachable'); +const upstreamResolve = run(`nslookup -timeout=3 google.com 8.8.8.8 2>&1`); +assert(!upstreamResolve || upstreamResolve.includes('connection timed out') || upstreamResolve.includes('no servers'), 'Upstream DNS unreachable before auth'); + +console.log('\n3. Per-client NAT filter: ping blocked'); +assert(!canPing(), 'Ping to 8.8.8.8 blocked by NAT filter'); + +console.log('\n4. Per-client NAT filter: HTTP blocked'); +const httpBefore = run(`curl -s --connect-timeout 5 -m 5 --interface wlp59s0 http://1.1.1.1/ 2>/dev/null`); +assert(!httpBefore || httpBefore.length === 0, 'HTTP blocked before auth'); + +console.log('\n5. Captive portal and API still accessible'); +const portal = run(`curl -s --connect-timeout 5 http://${IP}/`); +assert(portal && portal.includes('TollGate'), 'Portal HTML accessible'); +const apiDisc = runJson(`curl -s --connect-timeout 5 ${API}/`); +assert(apiDisc && apiDisc.kind === 10021, 'API discovery accessible'); + +console.log('\n--- Part 2: After Authentication ---\n'); + +console.log('6. Reset + Pay'); +run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`); +await sleep(1000); + +const token = mintToken(21); +assert(token !== null, 'Token generated'); +if (token) { + const payResult = runJson(`curl -s --connect-timeout 20 -X POST --data-binary '${token}' -H "Content-Type: application/cashu" ${API}/`); + assert(payResult && payResult.kind === 1022, 'Payment accepted'); +} + +await sleep(1000); + +console.log('\n7. DNS now forwards to upstream'); +assert(dnsResolveWorks('google.com'), 'DNS resolves to real IPs after auth'); + +console.log('\n8. Per-client NAT filter: ping allowed'); +assert(canPing(), 'Ping to 8.8.8.8 allowed after auth'); + +console.log('\n9. Per-client NAT filter: HTTP allowed'); +const httpAfter = run(`curl -s --connect-timeout 10 -m 10 --interface wlp59s0 http://1.1.1.1/ 2>/dev/null`); +assert(httpAfter && httpAfter.length > 0, 'HTTP allowed after auth'); + +console.log('\n--- Part 3: After Revocation ---\n'); + +console.log('10. Reset auth'); +run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`); +await sleep(1000); + +console.log('\n11. DNS goes back to hijack'); +assert(dnsResolvesToSelf('google.com'), 'DNS hijack restored after revoke'); + +console.log('\n12. Per-client NAT filter: ping blocked again'); +assert(!canPing(), 'Ping blocked after revoke'); + +console.log('\n13. Per-client NAT filter: HTTP blocked again'); +const httpRevoke = run(`curl -s --connect-timeout 5 -m 5 --interface wlp59s0 http://1.1.1.1/ 2>/dev/null`); +assert(!httpRevoke || httpRevoke.length === 0, 'HTTP blocked after revoke'); + +function dnsResolveWorks(domain) { + const result = run(`nslookup -timeout=3 ${domain} 2>&1`); + return result && result.includes('Address') && !result.includes(IP) && !result.includes('NXDOMAIN'); +} + +console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`); +process.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 @@ +import { execSync } from 'child_process'; + +const IP = process.env.TOLLGATE_IP || '10.192.45.1'; +const API = `http://${IP}:2121`; +const SUDO_PW = process.env.SUDO_PW || 'c03rad0r123'; +let passed = 0, failed = 0; + +function assert(cond, msg) { + if (cond) { console.log(` ✓ ${msg}`); passed++; } + else { console.log(` ✗ ${msg}`); failed++; } +} + +function run(cmd) { + try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); } + catch { return null; } +} + +function runJson(cmd) { + const out = run(cmd); + try { return out ? JSON.parse(out) : null; } + catch { return null; } +} + +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } + +function mintToken(amount = 21) { + run('cashu -h https://testnut.cashu.space invoice ' + amount + ' 2>&1'); + const out = run('cashu -h https://testnut.cashu.space send --legacy ' + amount + ' 2>&1'); + const match = out && out.match(/cashuA[a-zA-Z0-9_-]+/); + return match ? match[0] : null; +} + +function canPing(host = '8.8.8.8') { + const result = run(`ping -c 1 -W 2 -I wlp59s0 ${host} 2>/dev/null`); + return result && !result.includes('100% packet loss'); +} + +console.log(`\n=== Reset Auth Integration Test (target: ${IP}) ===\n`); + +console.log('1. Reset auth to clear state'); +const reset1 = run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`); +assert(reset1 && reset1.includes('reset'), 'Reset returns {"status":"reset"}'); + +await sleep(1000); + +console.log('\n2. Verify no session'); +const usage1 = run(`curl -s --connect-timeout 10 ${API}/usage`); +assert(usage1 && usage1.includes('-1/-1'), 'Usage is -1/-1 before payment'); + +console.log('\n3. Verify internet blocked'); +assert(!canPing(), 'Ping blocked before payment'); + +console.log('\n4. Pay with valid token'); +const token = mintToken(21); +assert(token !== null, 'Token generated'); +if (token) { + const payResult = runJson(`curl -s --connect-timeout 20 -X POST --data-binary '${token}' -H "Content-Type: application/cashu" ${API}/`); + assert(payResult && payResult.kind === 1022, 'Payment accepted (kind=1022)'); + const allotment = payResult && payResult.tags && payResult.tags.find(t => t[0] === 'allotment'); + assert(allotment && parseInt(allotment[1]) > 0, `Allotment: ${allotment ? allotment[1] : 'N/A'}ms`); +} + +await sleep(1000); + +console.log('\n5. Verify session active'); +const usage2 = run(`curl -s --connect-timeout 10 ${API}/usage`); +assert(usage2 && !usage2.includes('-1/-1'), `Usage: ${usage2}`); + +console.log('\n6. Verify internet allowed'); +assert(canPing(), 'Ping works with active session'); + +console.log('\n7. Reset auth while session active'); +const reset2 = run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`); +assert(reset2 && reset2.includes('reset'), 'Reset returns {"status":"reset"}'); + +await sleep(1000); + +console.log('\n8. Verify session cleared'); +const usage3 = run(`curl -s --connect-timeout 10 ${API}/usage`); +assert(usage3 && usage3.includes('-1/-1'), 'Usage is -1/-1 after reset'); + +console.log('\n9. Verify internet blocked again'); +assert(!canPing(), 'Ping blocked after reset'); + +console.log('\n10. Pay again (new token)'); +const token2 = mintToken(21); +if (token2) { + const pay2 = runJson(`curl -s --connect-timeout 20 -X POST --data-binary '${token2}' -H "Content-Type: application/cashu" ${API}/`); + assert(pay2 && pay2.kind === 1022, 'Second payment accepted'); +} + +await sleep(1000); + +console.log('\n11. Verify internet works again'); +assert(canPing(), 'Ping works with new session'); + +console.log('\n12. Final reset'); +run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`); + +console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`); +process.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 @@ +import { execSync } from 'child_process'; + +const IP = process.env.TOLLGATE_IP || '10.192.45.1'; +const API = `http://${IP}:2121`; +let passed = 0, failed = 0; + +function assert(cond, msg) { + if (cond) { console.log(` ✓ ${msg}`); passed++; } + else { console.log(` ✗ ${msg}`); failed++; } +} + +function run(cmd) { + try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); } + catch { return null; } +} + +function runJson(cmd) { + const out = run(cmd); + try { return out ? JSON.parse(out) : null; } + catch { return null; } +} + +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } + +function mintToken(amount = 21) { + run('cashu -h https://testnut.cashu.space invoice ' + amount + ' 2>&1'); + const out = run('cashu -h https://testnut.cashu.space send --legacy ' + amount + ' 2>&1'); + const match = out && out.match(/cashuA[a-zA-Z0-9_-]+/); + return match ? match[0] : null; +} + +function canPing(host = '8.8.8.8') { + const result = run(`ping -c 1 -W 2 -I wlp59s0 ${host} 2>/dev/null`); + return result && !result.includes('100% packet loss'); +} + +console.log(`\n=== Session Expiry Integration Test (target: ${IP}) ===`); +console.log(`NOTE: This test waits 65s for session expiry. Total runtime ~80s.\n`); + +console.log('1. Reset auth'); +run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`); + +await sleep(1000); + +console.log('\n2. Verify blocked before payment'); +assert(!canPing(), 'Ping blocked before payment'); + +const usage0 = run(`curl -s --connect-timeout 10 ${API}/usage`); +assert(usage0 && usage0.includes('-1/-1'), 'Usage is -1/-1'); + +console.log('\n3. Pay with valid token (21 sats = 60000ms)'); +const token = mintToken(21); +assert(token !== null, 'Token generated'); +if (token) { + const payResult = runJson(`curl -s --connect-timeout 20 -X POST --data-binary '${token}' -H "Content-Type: application/cashu" ${API}/`); + assert(payResult && payResult.kind === 1022, 'Payment accepted'); +} + +await sleep(1000); + +console.log('\n4. Verify session active'); +const usage1 = run(`curl -s --connect-timeout 10 ${API}/usage`); +assert(usage1 && !usage1.includes('-1/-1'), `Usage: ${usage1}`); + +console.log('\n5. Verify internet works'); +assert(canPing(), 'Ping works with active session'); + +const httpResult = run(`curl -s --connect-timeout 10 -m 10 --interface wlp59s0 http://1.1.1.1/ 2>/dev/null`); +assert(httpResult && httpResult.length > 0, 'HTTP request reaches internet'); + +console.log('\n6. Waiting 65s for session expiry (allotment=60000ms)...'); +for (let i = 65; i > 0; i -= 5) { + process.stdout.write(`\r ${i}s remaining...`); + await sleep(Math.min(5000, i * 1000)); +} +console.log('\r Session should be expired now. '); + +console.log('\n7. Verify session expired'); +const usage2 = run(`curl -s --connect-timeout 10 ${API}/usage`); +assert(usage2 && usage2.includes('-1/-1'), `Usage after expiry: ${usage2}`); + +console.log('\n8. Verify internet blocked after expiry'); +assert(!canPing(), 'Ping blocked after session expiry'); + +const httpResult2 = run(`curl -s --connect-timeout 5 -m 5 --interface wlp59s0 http://1.1.1.1/ 2>/dev/null`); +assert(!httpResult2 || httpResult2.length === 0, 'HTTP blocked after expiry'); + +console.log('\n9. Pay again to verify renewal works'); +const token2 = mintToken(21); +if (token2) { + const pay2 = runJson(`curl -s --connect-timeout 20 -X POST --data-binary '${token2}' -H "Content-Type: application/cashu" ${API}/`); + assert(pay2 && pay2.kind === 1022, 'Renewal payment accepted'); +} + +await sleep(1000); + +console.log('\n10. Verify internet works after renewal'); +assert(canPing(), 'Ping works after renewal'); + +run(`curl -s --connect-timeout 10 http://${IP}/reset_authentication`); + +console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`); +process.exit(failed > 0 ? 1 : 0); -- cgit v1.2.3