upleb.uk

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

summaryrefslogtreecommitdiff
path: root/tests/integration
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-17 17:18:43 +0530
committerYour Name <you@example.com>2026-05-17 17:18:43 +0530
commit8071741815f0b0938701e80a63e80b0ec94b2778 (patch)
tree2a1511480e0b58f4efb144aa9d10c9fba5eed034 /tests/integration
parent0c2c67b463d6a90aaa0bb69bf3c91dba1d9ec3ec (diff)
refactor: reorganize test suite, add integration tests for NAT filter
- 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
Diffstat (limited to 'tests/integration')
-rw-r--r--tests/integration/api.mjs79
-rw-r--r--tests/integration/network.mjs66
-rw-r--r--tests/integration/phase2.mjs151
-rw-r--r--tests/integration/smoke.mjs52
-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
7 files changed, 675 insertions, 0 deletions
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 @@
1import { curl, curlBody, getPortalIP, canPing, canResolve, dnsResolvesToSelf } from './helpers/network.mjs';
2
3const IP = getPortalIP();
4let passed = 0, failed = 0;
5
6function assert(condition, test) {
7 if (condition) { console.log(` ✓ ${test}`); passed++; }
8 else { console.log(` ✗ ${test}`); failed++; }
9}
10
11async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
12
13console.log(`\n=== API Tests (target: ${IP}) ===\n`);
14
15// Test 3: Captive portal serves HTML
16console.log('Test 3: GET / returns portal HTML');
17const body3 = curlBody(`http://${IP}/`);
18assert(body3 && body3.includes('TollGate'), 'Portal HTML contains "TollGate"');
19assert(body3 && body3.includes('Grant Free Access'), 'Portal has Grant Access button');
20
21// Test 4: Captive detection URIs
22console.log('\nTest 4: Captive detection URIs');
23for (const uri of ['/generate_204', '/hotspot-detect.html', '/canonical.html', '/success.txt', '/ncsi.txt', '/connecttest.txt', '/wpad.dat', '/redirect']) {
24 const code = curl(`http://${IP}${uri}`);
25 assert(code === '200', `${uri} → 200`);
26}
27
28// Test 7: /whoami returns MAC
29console.log('\nTest 7: GET /whoami');
30const body7 = curlBody(`http://${IP}/whoami`);
31assert(body7 && body7.startsWith('mac='), '/whoami returns mac=...');
32
33// Test 8: /usage returns no session
34console.log('\nTest 8: GET /usage');
35const body8 = curlBody(`http://${IP}/usage`);
36assert(body8 && body8.includes('-1/-1'), '/usage returns -1/-1 before auth');
37
38// Test 5: DNS hijack before auth
39console.log('\nTest 5: DNS hijack before auth');
40assert(dnsResolvesToSelf('google.com'), 'DNS resolves google.com to AP IP');
41
42// Test 6: No internet before auth
43console.log('\nTest 6: No internet before auth');
44assert(!canPing('8.8.8.8', 1), 'ping 8.8.8.8 fails before auth');
45
46// Test 9: Grant access
47console.log('\nTest 9: GET /grant_access');
48const body9 = curlBody(`http://${IP}/grant_access`);
49assert(body9 && body9.includes('"granted"'), 'Grant access returns {"status":"granted"}');
50
51await sleep(2000);
52
53// Test 10: DNS forward after auth
54console.log('\nTest 10: DNS forward after auth');
55assert(canResolve('google.com'), 'DNS resolves normally after auth');
56
57// Test 11: Internet after auth
58console.log('\nTest 11: Internet after auth');
59assert(canPing('8.8.8.8'), 'ping 8.8.8.8 succeeds after auth');
60
61// Test 12: HTTP browsing works
62console.log('\nTest 12: HTTP browsing');
63const body12 = curlBody('http://example.com/');
64assert(body12 && (body12.includes('Example Domain') || body12.includes('example')), 'HTTP page loads');
65
66// Test 13: Reset auth
67console.log('\nTest 13: GET /reset_authentication');
68const body13 = curlBody(`http://${IP}/reset_authentication`);
69assert(body13 && body13.includes('"reset"'), 'Reset returns {"status":"reset"}');
70
71await sleep(2000);
72
73// Test 14: Internet blocked after reset
74console.log('\nTest 14: Internet blocked after reset');
75assert(!canPing('8.8.8.8', 1), 'ping fails after auth reset');
76
77// Summary
78console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`);
79process.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 @@
1import { execSync } from 'child_process';
2
3const IP = process.env.TOLLGATE_IP || '10.192.45.1';
4let passed = 0, failed = 0;
5
6function assert(condition, test) {
7 if (condition) { console.log(` ✓ ${test}`); passed++; }
8 else { console.log(` ✗ ${test}`); failed++; }
9}
10
11function run(cmd) {
12 try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); }
13 catch { return null; }
14}
15
16console.log(`\n=== Network Tests (target: ${IP}) ===\n`);
17
18// Test 1: AP visible in scan
19console.log('Test 1: AP visible in scan');
20const scan = run('nmcli -t -f SSID dev wifi list 2>/dev/null');
21assert(scan && scan.includes('TollGate'), 'TollGate SSID visible in WiFi scan');
22
23// Test 2: DHCP lease
24console.log('\nTest 2: DHCP lease / connectivity');
25const ip_show = run(`ip addr show | grep "inet ${IP.split('.').slice(0,3).join('.')}"`);
26assert(ip_show !== null, `Has IP in ${IP.split('.').slice(0,3).join('.')}.* subnet`);
27
28// Test 5: DNS hijack
29console.log('\nTest 5: DNS hijack before auth');
30const ns1 = run(`nslookup random-test.example.com ${IP} 2>/dev/null`);
31assert(ns1 && ns1.includes(IP), 'DNS resolves arbitrary domain to AP IP');
32
33// Test 6: No internet
34console.log('\nTest 6: No internet before auth');
35const ping1 = run('ping -c 1 -W 3 1.1.1.1 2>/dev/null');
36assert(ping1 === null || ping1.includes('100% packet loss'), 'Internet blocked before auth');
37
38// Grant access for further tests
39console.log('\nGranting access...');
40run(`curl -s http://${IP}/grant_access`);
41
42import { execSync as exec } from 'child_process';
43await new Promise(r => setTimeout(r, 2000));
44
45// Test 10: DNS forward
46console.log('Test 10: DNS forward after auth');
47const ns2 = run(`nslookup google.com ${IP} 2>/dev/null`);
48assert(ns2 && !ns2.includes(IP) && ns2.includes('Address'), 'DNS resolves to real IPs');
49
50// Test 11: Internet
51console.log('\nTest 11: Internet after auth');
52const ping2 = run('ping -c 2 -W 3 8.8.8.8');
53assert(ping2 && !ping2.includes('100% packet loss'), 'ping succeeds after auth');
54
55// Reset
56console.log('\nResetting auth...');
57run(`curl -s http://${IP}/reset_authentication`);
58await new Promise(r => setTimeout(r, 2000));
59
60// Test 14
61console.log('Test 14: Internet blocked after reset');
62const ping3 = run('ping -c 1 -W 3 8.8.8.8 2>/dev/null');
63assert(ping3 === null || ping3.includes('100% packet loss'), 'Internet blocked after reset');
64
65console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`);
66process.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 @@
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(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(1500);
76 const sudoPw = process.env.SUDO_PW || 'c03rad0r123';
77 try {
78 execSync(`echo '${sudoPw}' | sudo -S ip route add default via ${IP} dev wlp59s0 metric 50 2>/dev/null`, { encoding: 'utf8', timeout: 5000 });
79 } catch {}
80 let pingOk = false;
81 try {
82 const ping18 = execSync('ping -c 3 -W 3 8.8.8.8', { encoding: 'utf8', timeout: 15000 });
83 pingOk = ping18 && !ping18.includes('100% packet loss');
84 } catch {
85 pingOk = false;
86 }
87 assert(pingOk, 'Internet works');
88
89 // Test 20: Spent token
90 console.log('\nTest 20: Reuse token (should fail)');
91 const body20 = curlBody(`${API}/`, { method: 'POST', data: TEST_TOKEN });
92 const json20 = body20 ? JSON.parse(body20) : null;
93 assert(json20 && json20.kind === 21023, 'Returns kind=21023 for spent token');
94
95 // Test 22: Session expiry
96 console.log('\nTest 22: Session expiry (waiting 65s for allotment to expire)...');
97 try {
98 execSync(`echo '${sudoPw}' | sudo -S ip route add default via ${IP} dev wlp59s0 metric 50 2>/dev/null`, { encoding: 'utf8', timeout: 5000 });
99 } catch {}
100 await sleep(65000);
101 let expiredPingOk = true;
102 try {
103 const ping22 = execSync('ping -c 2 -W 2 8.8.8.8', { encoding: 'utf8', timeout: 10000 });
104 expiredPingOk = !ping22.includes('100% packet loss');
105 } catch {
106 expiredPingOk = false;
107 }
108 assert(!expiredPingOk, 'Internet blocked after session expiry');
109 const body22 = curlBody(`${API}/usage`);
110 assert(body22 && body22.includes('-1/-1'), 'Usage returns -1/-1 after expiry');
111
112 // Test 23: Session renewal
113 const TEST_TOKEN2 = process.env.TEST_TOKEN2;
114 if (TEST_TOKEN2) {
115 console.log('\nTest 23: Session renewal with second token');
116 const body23 = curlBody(`${API}/`, { method: 'POST', data: TEST_TOKEN2 });
117 const json23 = body23 ? JSON.parse(body23) : null;
118 assert(json23 && json23.kind === 1022, 'Returns kind=1022 for renewal');
119 await sleep(1500);
120 let renewPingOk = false;
121 try {
122 const ping23 = execSync('ping -c 2 -W 2 8.8.8.8', { encoding: 'utf8', timeout: 10000 });
123 renewPingOk = !ping23.includes('100% packet loss');
124 } catch {
125 renewPingOk = false;
126 }
127 assert(renewPingOk, 'Internet works after renewal');
128 } else {
129 console.log('\n ⚠ Skipping test 23: Set TEST_TOKEN2 env var for renewal test');
130 }
131 try {
132 execSync(`echo '${sudoPw}' | sudo -S ip route del default via ${IP} dev wlp59s0 metric 50 2>/dev/null`, { encoding: 'utf8', timeout: 5000 });
133 } catch {}
134} else {
135 console.log('\n ⚠ Skipping tests 16-20: Set TEST_TOKEN env var with a valid Cashu token');
136}
137
138// Test: whoami on :2121
139console.log('\nTest: GET :2121/whoami');
140const bodyWhoami = curlBody(`${API}/whoami`);
141assert(bodyWhoami && bodyWhoami.includes('mac='), '/whoami returns mac=...');
142
143// Test: Portal has payment form
144console.log('\nTest: Portal has payment form');
145const bodyPortal = curlBody(`http://${IP}/`);
146assert(bodyPortal && bodyPortal.includes('cashuA'), 'Portal has Cashu token input');
147assert(bodyPortal && bodyPortal.includes('Pay &amp; Connect') || bodyPortal && bodyPortal.includes('Pay'), 'Portal has Pay button');
148
149// Summary
150console.log(`\n=== Phase 2 Results: ${passed} passed, ${failed} failed ===\n`);
151process.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 @@
1import { execSync } from 'child_process';
2
3const PORT = process.argv[2] || '/dev/ttyACM0';
4const IP = process.env.TOLLGATE_IP || '10.192.45.1';
5const SSID = process.env.AP_SSID || 'TollGate';
6
7console.log(`\n=== Smoke Test (30s) ===`);
8console.log(`Port: ${PORT}, Portal IP: ${IP}, SSID: ${SSID}\n`);
9
10let passed = 0, failed = 0;
11function assert(cond, msg) {
12 if (cond) { console.log(` ✓ ${msg}`); passed++; }
13 else { console.log(` ✗ ${msg}`); failed++; }
14}
15
16function run(cmd) {
17 try { return execSync(cmd, { encoding: 'utf8', timeout: 10000 }); }
18 catch { return null; }
19}
20
21// 1. Check AP visible
22const scan = run('nmcli -t -f SSID dev wifi list 2>/dev/null');
23assert(scan && scan.includes(SSID), `SSID "${SSID}" visible`);
24
25// 2. Check we can reach portal
26const portal = run(`curl -s --connect-timeout 5 http://${IP}/`);
27assert(portal && portal.includes('TollGate'), 'Portal HTML loads');
28
29// 3. Grant access
30const grant = run(`curl -s http://${IP}/grant_access`);
31assert(grant && grant.includes('granted'), 'Grant access works');
32
33// Wait for DNS
34const sleep = ms => new Promise(r => setTimeout(r, ms));
35await sleep(2000);
36
37// 4. Internet works
38const ping = run('ping -c 1 -W 3 -I wlp59s0 1.1.1.1 2>/dev/null');
39assert(ping && !ping.includes('100% packet loss'), 'Internet works after grant');
40
41// 5. Reset
42const reset = run(`curl -s http://${IP}/reset_authentication`);
43assert(reset && reset.includes('reset'), 'Reset auth works');
44
45await sleep(2000);
46
47// 6. Internet blocked
48const ping2 = run('ping -c 1 -W 3 -I wlp59s0 1.1.1.1 2>/dev/null');
49assert(!ping2 || ping2.includes('100% packet loss'), 'Internet blocked after reset');
50
51console.log(`\n=== Smoke: ${passed} passed, ${failed} failed ===\n`);
52process.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 @@
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);