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-17 17:18:43 +0530
committerYour Name <you@example.com>2026-05-17 17:18:43 +0530
commit8071741815f0b0938701e80a63e80b0ec94b2778 (patch)
tree2a1511480e0b58f4efb144aa9d10c9fba5eed034 /tests
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')
-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
11 files changed, 333 insertions, 6 deletions
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);