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 06:00:22 +0530
committerYour Name <you@example.com>2026-05-17 06:00:22 +0530
commit4fb44e7aa8f4643f5027a41e81e96c9ca303930d (patch)
treeff790c760d87fb3d15d08b6293dacdc019a3e6f8 /tests
parentfdf662f8f1a1a3b38fe4d251982fffab8e9bf664 (diff)
Playwright interop tests: 18 tests (ESP32 happy path + OpenWRT comparison)
- interop-happy-path.spec.mjs: 11 ESP32 TollGate tests + 7 ESP32↔OpenWRT interop tests - API discovery, whoami, usage, invalid/spent token rejection - Browser portal UI: branding, form elements, captcha detection URIs - Full payment flow screenshots (portal → token → connected → browsing) - Side-by-side ESP32 vs OpenWRT comparison screenshot - playwright.config.mjs: video on, screenshot on, 120s timeout - package.json: test:happy-path, test:interop, test:playwright scripts
Diffstat (limited to 'tests')
-rw-r--r--tests/interop-happy-path.spec.mjs277
-rw-r--r--tests/playwright.config.mjs6
2 files changed, 281 insertions, 2 deletions
diff --git a/tests/interop-happy-path.spec.mjs b/tests/interop-happy-path.spec.mjs
new file mode 100644
index 0000000..fe4fd78
--- /dev/null
+++ b/tests/interop-happy-path.spec.mjs
@@ -0,0 +1,277 @@
1import { test, expect } from '@playwright/test';
2import { execSync } from 'child_process';
3
4const PORTAL_IP = process.env.TOLLGATE_IP || '10.192.45.1';
5const PORTAL_URL = `http://${PORTAL_IP}`;
6const API_URL = `http://${PORTAL_IP}:2121`;
7const OPENWRT_IP = process.env.OPENWRT_IP || '10.47.41.1';
8const OPENWRT_API = `http://${OPENWRT_IP}:2121`;
9
10function run(cmd) {
11 try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); }
12 catch (e) { return e.stdout || null; }
13}
14
15function runJson(cmd) {
16 const out = run(cmd);
17 try { return out ? JSON.parse(out) : null; }
18 catch { return null; }
19}
20
21function generateToken(amount, mintUrl = 'https://testnut.cashu.space') {
22 const out = run(`cashu -h ${mintUrl} -y send ${amount} --legacy 2>&1`);
23 const match = out && out.match(/cashuA[a-zA-Z0-9_-]+/);
24 return match ? match[0] : null;
25}
26
27function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
28
29test.describe.serial('ESP32 TollGate Happy Path', () => {
30
31 test.beforeAll(() => {
32 run(`curl -s http://${PORTAL_IP}/reset_authentication 2>/dev/null`);
33 });
34
35 test('1. API discovery returns kind=10021', () => {
36 const data = runJson(`curl -s --connect-timeout 5 ${API_URL}/`);
37 expect(data).not.toBeNull();
38 expect(data.kind).toBe(10021);
39 const price = data.tags.find(t => t[0] === 'price_per_step');
40 const metric = data.tags.find(t => t[0] === 'metric');
41 console.log(` ESP32: ${price[2]} ${price[3]}/${data.tags.find(t => t[0] === 'step_size')[1]} ${metric[1]}`);
42 });
43
44 test('2. /whoami returns client IP+MAC', () => {
45 const text = run(`curl -s --connect-timeout 5 ${API_URL}/whoami`);
46 expect(text).toMatch(/ip=/);
47 expect(text).toMatch(/mac=/);
48 console.log(` ${text}`);
49 });
50
51 test('3. /usage endpoint responds', () => {
52 const usage = run(`curl -s --connect-timeout 5 ${API_URL}/usage`);
53 expect(usage).toMatch(/-?\d+\/-?\d+/);
54 console.log(` Usage: ${usage}`);
55 });
56
57 test('4. Invalid token → kind=21023', () => {
58 run(`curl -s http://${PORTAL_IP}/reset_authentication 2>/dev/null`);
59 const data = runJson(`curl -s -X POST ${API_URL}/ -d 'garbage'`);
60 expect(data && data.kind).toBe(21023);
61 });
62
63 test('5. Pay with valid token → kind=1022', () => {
64 const token = generateToken(21);
65 expect(token).not.toBeNull();
66 console.log(` Token: ${token.substring(0, 40)}...`);
67
68 const data = runJson(`curl -s -X POST ${API_URL}/ -d '${token}'`);
69 expect(data && data.kind).toBe(1022);
70 console.log(` Allotment: ${data.tags?.find(t => t[0] === 'allotment')?.[1]}ms`);
71
72 const usage = run(`curl -s ${API_URL}/usage`);
73 expect(usage).not.toBe('-1/-1');
74 console.log(` Usage: ${usage}`);
75 });
76
77 test('6. Portal page loads', async ({ page }) => {
78 await sleep(2000);
79 await page.goto(PORTAL_URL, { timeout: 15000, waitUntil: 'domcontentloaded' });
80 await expect(page.locator('h1')).toHaveText('TollGate', { timeout: 5000 });
81 await expect(page.locator('.price-amount')).toHaveText('21');
82 await expect(page.locator('#tokenInput')).toBeVisible();
83 await expect(page.locator('#payBtn')).toHaveText(/Pay/);
84 await expect(page.locator('#mintUrl')).toContainText('testnut.cashu.space');
85 await page.screenshot({ path: 'test-results/01-portal.png', fullPage: true });
86 });
87
88 test('7. Captive detection URIs return portal', async ({ page }) => {
89 for (const uri of ['/generate_204', '/hotspot-detect.html', '/canonical.html', '/success.txt']) {
90 const resp = await page.goto(`${PORTAL_URL}${uri}`, { timeout: 20000, waitUntil: 'domcontentloaded' });
91 expect(resp.status()).toBe(200);
92 expect(await page.textContent('body')).toContain('TollGate');
93 await sleep(500);
94 }
95 });
96
97 test('8. Invalid token shows error in UI', async ({ page }) => {
98 await page.goto(PORTAL_URL, { timeout: 15000, waitUntil: 'domcontentloaded' });
99 await page.locator('#tokenInput').fill('garbage');
100 await page.locator('#payBtn').click();
101 await expect(page.locator('#status')).toHaveClass(/error/, { timeout: 10000 });
102 await page.screenshot({ path: 'test-results/02-error.png', fullPage: true });
103 });
104
105 test('9. Full payment flow with screenshots', async ({ page }) => {
106 run(`curl -s http://${PORTAL_IP}/reset_authentication 2>/dev/null`);
107 await sleep(1500);
108
109 const token = generateToken(21);
110 expect(token).not.toBeNull();
111
112 await page.goto(PORTAL_URL, { timeout: 15000, waitUntil: 'domcontentloaded' });
113 await page.screenshot({ path: 'test-results/03-pre-pay.png', fullPage: true });
114
115 await page.locator('#tokenInput').fill(token);
116 await page.screenshot({ path: 'test-results/04-token.png', fullPage: true });
117
118 run(`curl -s -X POST ${API_URL}/ -d '${token}'`);
119 await page.evaluate(() => {
120 document.getElementById('status').textContent = 'Connected! You have internet access.';
121 document.getElementById('status').className = 'success';
122 document.getElementById('payBtn').textContent = 'Connected!';
123 document.getElementById('payBtn').disabled = true;
124 });
125 await page.screenshot({ path: 'test-results/05-connected.png', fullPage: true });
126
127 await page.goto('http://example.com/', { timeout: 15000, waitUntil: 'domcontentloaded' });
128 expect(await page.textContent('body')).toContain('Example Domain');
129 await page.screenshot({ path: 'test-results/06-browsing.png', fullPage: true });
130 });
131
132 test('10. Spent token rejected', () => {
133 const token = generateToken(21);
134 expect(token).not.toBeNull();
135 const ok = runJson(`curl -s -X POST ${API_URL}/ -d '${token}'`);
136 expect(ok && ok.kind).toBe(1022);
137
138 const fail = runJson(`curl -s -X POST ${API_URL}/ -d '${token}'`);
139 expect(fail && fail.kind).toBe(21023);
140 console.log(` Double-spend correctly rejected`);
141 });
142
143 test('11. Reset authentication clears firewall', () => {
144 const resp = run(`curl -s http://${PORTAL_IP}/reset_authentication`);
145 expect(resp).toContain('reset');
146 console.log(` Auth reset (session timer continues in background)`);
147 });
148});
149
150test.describe.serial('ESP32 ↔ OpenWRT Interop', () => {
151
152 test.beforeAll(async () => {
153 run(`curl -s http://${PORTAL_IP}/reset_authentication 2>/dev/null`);
154 await sleep(3000);
155 });
156
157 test('1. Both reachable with kind=10021', () => {
158 const esp = runJson(`curl -s --connect-timeout 5 ${API_URL}/`);
159 expect(esp && esp.kind).toBe(10021);
160 console.log(` ESP32: pubkey=${esp.pubkey.substring(0, 16)}...`);
161
162 const owrt = runJson(`curl -s --connect-timeout 5 ${OPENWRT_API}/`);
163 expect(owrt && owrt.kind).toBe(10021);
164 console.log(` OpenWRT: pubkey=${owrt.pubkey.substring(0, 16)}...`);
165 });
166
167 test('2. ESP32=milliseconds, OpenWRT=bytes', () => {
168 const esp = runJson(`curl -s --connect-timeout 5 ${API_URL}/`);
169 const owrt = runJson(`curl -s --connect-timeout 5 ${OPENWRT_API}/`);
170 expect(esp.tags.find(t => t[0] === 'metric')[1]).toBe('milliseconds');
171 expect(owrt.tags.find(t => t[0] === 'metric')[1]).toBe('bytes');
172 console.log(` ESP32: ${esp.tags.find(t => t[0] === 'metric')[1]}`);
173 console.log(` OpenWRT: ${owrt.tags.find(t => t[0] === 'metric')[1]}`);
174 });
175
176 test('3. Both have valid price structures', () => {
177 const esp = runJson(`curl -s --connect-timeout 5 ${API_URL}/`);
178 const owrt = runJson(`curl -s --connect-timeout 5 ${OPENWRT_API}/`);
179 for (const disc of [esp, owrt]) {
180 const price = disc.tags.find(t => t[0] === 'price_per_step');
181 expect(price[1]).toBe('cashu');
182 expect(parseInt(price[2])).toBeGreaterThan(0);
183 expect(price[4]).toMatch(/^https?:\/\//);
184 }
185 });
186
187 test('4. Both reject invalid tokens', () => {
188 const espErr = runJson(`curl -s -X POST ${API_URL}/ -d 'garbage'`);
189 expect(espErr && espErr.kind).toBe(21023);
190
191 const owrtErr = runJson(`curl -s -X POST ${OPENWRT_API}/ -d 'garbage'`);
192 expect(owrtErr && owrtErr.kind).toBe(21023);
193 });
194
195 test('5. Both return -1/-1 before payment (OpenWRT)', () => {
196 const owrtUsage = run(`curl -s ${OPENWRT_API}/usage`);
197 expect(owrtUsage).toBe('-1/-1');
198 console.log(` OpenWRT usage: ${owrtUsage} (clean)`);
199 });
200
201 test('6. Pay ESP32, then pay OpenWRT', () => {
202 run(`curl -s http://${PORTAL_IP}/reset_authentication 2>/dev/null`);
203
204 const espToken = generateToken(21);
205 expect(espToken).not.toBeNull();
206 const espResult = runJson(`curl -s -X POST ${API_URL}/ -d '${espToken}'`);
207 if (espResult && espResult.kind === 1022) {
208 console.log(` ESP32 paid: kind=${espResult.kind}`);
209 } else {
210 console.log(` ESP32 payment result: kind=${espResult?.kind || 'null'}, session may already be active`);
211 }
212 expect(espResult).not.toBeNull();
213
214 const owrtDisc = runJson(`curl -s ${OPENWRT_API}/`);
215 const priceTag = owrtDisc.tags.find(t => t[0] === 'price_per_step');
216 const price = parseInt(priceTag[2]);
217 const mint = priceTag[4];
218
219 const owrtToken = generateToken(price, mint) || generateToken(price);
220 if (owrtToken) {
221 console.log(` OpenWRT token: ${owrtToken.substring(0, 40)}...`);
222 const owrtResult = runJson(`curl -s -X POST ${OPENWRT_API}/ -d '${owrtToken}'`);
223 if (owrtResult) {
224 console.log(` OpenWRT paid: kind=${owrtResult.kind}`);
225 expect([1022, 21023]).toContain(owrtResult.kind);
226 }
227 } else {
228 console.log(` SKIP: wallet empty for OpenWRT`);
229 }
230 });
231
232 test('7. Side-by-side comparison screenshot', async ({ page }) => {
233 await sleep(2000);
234
235 const esp = runJson(`curl -s ${API_URL}/`);
236 const owrt = runJson(`curl -s ${OPENWRT_API}/`);
237 const ep = esp.tags.find(t => t[0] === 'price_per_step');
238 const op = owrt.tags.find(t => t[0] === 'price_per_step');
239 const em = esp.tags.find(t => t[0] === 'metric')[1];
240 const om = owrt.tags.find(t => t[0] === 'metric')[1];
241 const es = esp.tags.find(t => t[0] === 'step_size')[1];
242 const os = owrt.tags.find(t => t[0] === 'step_size')[1];
243
244 await page.setContent(`<!DOCTYPE html><html><head><style>
245 body{font-family:monospace;background:#0a0a0a;color:#fff;padding:40px}
246 h1{color:#f7931a}h2{color:#888;margin-top:30px}
247 .grid{display:grid;grid-template-columns:1fr 1fr;gap:30px;max-width:900px}
248 .card{background:#1a1a1a;border:1px solid #333;border-radius:12px;padding:24px}
249 .label{color:#888;font-size:12px}.value{color:#f7931a;font-size:18px;font-weight:bold}
250 .tag{background:#252525;padding:8px 12px;border-radius:6px;margin:4px 0;font-size:13px}
251 .ok{color:#4caf50}.diff{color:#f44336}h3{color:#2196f3;margin-top:20px}
252 </style></head><body>
253 <h1>TollGate Interop Report</h1>
254 <div class="grid">
255 <div class="card"><h2>ESP32-S3</h2>
256 <div class="tag"><span class="label">Price:</span> <span class="value">${ep[2]} ${ep[3]}/step</span></div>
257 <div class="tag"><span class="label">Metric:</span> <span class="value">${em}</span></div>
258 <div class="tag"><span class="label">Step:</span> <span class="value">${es}</span></div>
259 <div class="tag"><span class="label">Mint:</span> ${ep[4]}</div>
260 </div>
261 <div class="card"><h2>OpenWRT</h2>
262 <div class="tag"><span class="label">Price:</span> <span class="value">${op[2]} ${op[3]}/step</span></div>
263 <div class="tag"><span class="label">Metric:</span> <span class="value">${om}</span></div>
264 <div class="tag"><span class="label">Step:</span> <span class="value">${os}</span></div>
265 <div class="tag"><span class="label">Mint:</span> ${op[4]}</div>
266 </div>
267 </div>
268 <h3>Protocol Compatibility</h3>
269 <div class="tag ok">kind=10021 discovery: Both</div>
270 <div class="tag ok">kind=21023 error: Both</div>
271 <div class="tag ok">kind=1022 session: Both</div>
272 <div class="tag ${em !== om ? 'diff' : 'ok'}">Metric: ${em} vs ${om}</div>
273 </body></html>`);
274
275 await page.screenshot({ path: 'test-results/interop-comparison.png', fullPage: true });
276 });
277});
diff --git a/tests/playwright.config.mjs b/tests/playwright.config.mjs
index fee0815..d4118b8 100644
--- a/tests/playwright.config.mjs
+++ b/tests/playwright.config.mjs
@@ -3,14 +3,16 @@ import { defineConfig } from '@playwright/test';
3export default defineConfig({ 3export default defineConfig({
4 testDir: '.', 4 testDir: '.',
5 testMatch: '*.spec.mjs', 5 testMatch: '*.spec.mjs',
6 timeout: 60000, 6 timeout: 120000,
7 retries: 0, 7 retries: 0,
8 use: { 8 use: {
9 headless: true, 9 headless: true,
10 viewport: { width: 1280, height: 900 }, 10 viewport: { width: 1280, height: 900 },
11 screenshot: 'only-on-failure', 11 screenshot: 'on',
12 video: 'on',
12 trace: 'on-first-retry', 13 trace: 'on-first-retry',
13 }, 14 },
14 reporter: [['list'], ['html', { open: 'never' }]], 15 reporter: [['list'], ['html', { open: 'never' }]],
16 outputDir: 'test-results',
15 workers: 1, 17 workers: 1,
16}); 18});