1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
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(1500);
const sudoPw = process.env.SUDO_PW || 'c03rad0r123';
try {
execSync(`echo '${sudoPw}' | sudo -S ip route add default via 192.168.4.1 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;
}
try {
execSync(`echo '${sudoPw}' | sudo -S ip route del default via 192.168.4.1 dev wlp59s0 metric 50 2>/dev/null`, { encoding: 'utf8', timeout: 5000 });
} catch {}
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');
} 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);
|