upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHECKLIST.md6
-rw-r--r--PLAN.md4
-rw-r--r--tests/captive-portal.spec.mjs73
3 files changed, 46 insertions, 37 deletions
diff --git a/CHECKLIST.md b/CHECKLIST.md
index dc16b0b..2ac593c 100644
--- a/CHECKLIST.md
+++ b/CHECKLIST.md
@@ -58,12 +58,12 @@
58- [x] Test 19: Invalid token rejected (POST garbage → 400, kind=21023) — PASSING 58- [x] Test 19: Invalid token rejected (POST garbage → 400, kind=21023) — PASSING
59- [x] Test 20: Spent token rejected (reuse token → kind=21023) — PASSING 59- [x] Test 20: Spent token rejected (reuse token → kind=21023) — PASSING
60- [x] Test 21: Wrong mint rejected (POST token from wrong mint → kind=21023) — PASSING 60- [x] Test 21: Wrong mint rejected (POST token from wrong mint → kind=21023) — PASSING
61- [x] Test 22: Session expiry (wait for allotment → internet blocked) — PASSING
62- [x] Test 23: Session renewal (second payment → allotment extended) — PASSING
61- [x] Test: /whoami returns ip=X.X.X.X mac=XX:XX:XX:XX:XX:XX — PASSING 63- [x] Test: /whoami returns ip=X.X.X.X mac=XX:XX:XX:XX:XX:XX — PASSING
62- [x] Test: Portal has payment form (Cashu token input + Pay button) — PASSING 64- [x] Test: Portal has payment form (Cashu token input + Pay button) — PASSING
63 65
64### Tests Not Yet Run (need hardware + time) 66### Tests Not Yet Run (need Playwright)
65- [ ] Test 22: Session expiry (wait for allotment → internet blocked)
66- [ ] Test 23: Session renewal (second payment → allotment extended)
67- [ ] Test 24: Portal payment form visible in browser (Playwright) 67- [ ] Test 24: Portal payment form visible in browser (Playwright)
68- [ ] Test 25: Two clients pay independently 68- [ ] Test 25: Two clients pay independently
69- [ ] Test 26: Client isolation (only payer gets internet) 69- [ ] Test 26: Client isolation (only payer gets internet)
diff --git a/PLAN.md b/PLAN.md
index a62959d..2406158 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -72,8 +72,8 @@ Build a TollGate firmware for two ESP32 devices, following the [TollGate protoco
72| 19 | Invalid token | POST :2121/ garbage | kind=21023 error | PASS | 72| 19 | Invalid token | POST :2121/ garbage | kind=21023 error | PASS |
73| 20 | Spent token | Reuse token | kind=21023 spent error | PASS | 73| 20 | Spent token | Reuse token | kind=21023 spent error | PASS |
74| 21 | Wrong mint | Token from unaccepted mint | kind=21023 mint error | PASS | 74| 21 | Wrong mint | Token from unaccepted mint | kind=21023 mint error | PASS |
75| 22 | Session expiry | Wait for allotment | Internet blocked | TODO | 75| 22 | Session expiry | Wait for allotment | Internet blocked | PASS |
76| 23 | Session renewal | Second payment | Allotment extended | TODO | 76| 23 | Session renewal | Second payment | Allotment extended | PASS |
77| 24 | Portal payment form | Playwright paste token | Checkmark shown | TODO | 77| 24 | Portal payment form | Playwright paste token | Checkmark shown | TODO |
78| 25 | Two clients pay independently | Two POSTs | Both authenticated | TODO | 78| 25 | Two clients pay independently | Two POSTs | Both authenticated | TODO |
79| 26 | Client isolation | Only payer gets internet | Non-payer blocked | TODO | 79| 26 | Client isolation | Only payer gets internet | Non-payer blocked | TODO |
diff --git a/tests/captive-portal.spec.mjs b/tests/captive-portal.spec.mjs
index b6ad96b..acd2a40 100644
--- a/tests/captive-portal.spec.mjs
+++ b/tests/captive-portal.spec.mjs
@@ -2,8 +2,9 @@ import { 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 || '192.168.4.1';
4const PORTAL_URL = `http://${PORTAL_IP}`; 4const PORTAL_URL = `http://${PORTAL_IP}`;
5const API_URL = `http://${PORTAL_IP}:2121`;
5 6
6test.describe('Captive Portal - Phase 1', () => { 7test.describe('Captive Portal - Phase 2', () => {
7 8
8 test('portal page loads with TollGate branding', async ({ page }) => { 9 test('portal page loads with TollGate branding', async ({ page }) => {
9 await page.goto(PORTAL_URL); 10 await page.goto(PORTAL_URL);
@@ -11,65 +12,73 @@ test.describe('Captive Portal - Phase 1', () => {
11 await expect(page.locator('.subtitle')).toContainText('internet access'); 12 await expect(page.locator('.subtitle')).toContainText('internet access');
12 }); 13 });
13 14
14 test('portal shows price', async ({ page }) => { 15 test('portal shows price from API', async ({ page }) => {
15 await page.goto(PORTAL_URL); 16 await page.goto(PORTAL_URL);
16 const priceEl = page.locator('.price-amount'); 17 const priceEl = page.locator('.price-amount');
17 await expect(priceEl).not.toBeEmpty({ timeout: 5000 }); 18 await expect(priceEl).not.toBeEmpty({ timeout: 5000 });
18 }); 19 });
19 20
20 test('grant access button exists', async ({ page }) => { 21 test('portal has Cashu token input', async ({ page }) => {
21 await page.goto(PORTAL_URL); 22 await page.goto(PORTAL_URL);
22 const btn = page.locator('#grantBtn'); 23 const textarea = page.locator('#tokenInput');
23 await expect(btn).toBeVisible(); 24 await expect(textarea).toBeVisible();
24 await expect(btn).toHaveText(/Grant Free Access/i); 25 await expect(textarea).toHaveAttribute('placeholder', /cashuA/);
25 }); 26 });
26 27
27 test('click grant access shows connected', async ({ page }) => { 28 test('portal has Pay & Connect button', async ({ page }) => {
28 await page.goto(PORTAL_URL); 29 await page.goto(PORTAL_URL);
29 const btn = page.locator('#grantBtn'); 30 const btn = page.locator('#payBtn');
30 await btn.click(); 31 await expect(btn).toBeVisible();
31 const status = page.locator('#status.success'); 32 await expect(btn).toHaveText(/Pay/);
32 await expect(status).toBeVisible({ timeout: 10000 });
33 await expect(status).toContainText(/Connected/i);
34 }); 33 });
35 34
36 test('captive detection URIs return portal', async ({ page }) => { 35 test('captive detection URIs return 302 redirect', async ({ request }) => {
37 const uris = ['/generate_204', '/hotspot-detect.html', '/canonical.html', '/success.txt']; 36 const uris = ['/generate_204', '/hotspot-detect.html', '/canonical.html', '/success.txt'];
38 for (const uri of uris) { 37 for (const uri of uris) {
39 const resp = await page.goto(`${PORTAL_URL}${uri}`); 38 const resp = await request.fetch(`${PORTAL_URL}${uri}`, { maxRedirects: 0, ignoreHTTPSErrors: true });
40 expect(resp.status()).toBe(200); 39 expect(resp.status()).toBe(302);
41 const body = await resp.text(); 40 const location = resp.headers()['location'];
42 expect(body).toContain('TollGate'); 41 expect(location).toBe('http://192.168.4.1/');
43 } 42 }
44 }); 43 });
45 44
46 test('/api/status returns JSON with price', async ({ page }) => { 45 test('captive detection redirects to portal page', async ({ page }) => {
47 const resp = await page.goto(`${PORTAL_URL}/api/status`); 46 await page.goto(`${PORTAL_URL}/generate_204`);
48 expect(resp.status()).toBe(200); 47 await expect(page.locator('h1')).toHaveText('TollGate');
49 const data = await resp.json();
50 expect(data).toHaveProperty('connected');
51 expect(data).toHaveProperty('price');
52 expect(typeof data.price).toBe('number');
53 }); 48 });
54 49
55 test('/whoami returns mac address', async ({ page }) => { 50 test('/whoami returns ip and mac', async ({ page }) => {
56 const resp = await page.goto(`${PORTAL_URL}/whoami`); 51 const resp = await page.goto(`${API_URL}/whoami`);
57 expect(resp.status()).toBe(200); 52 expect(resp.status()).toBe(200);
58 const text = await resp.text(); 53 const text = await resp.text();
59 expect(text).toMatch(/^mac=/); 54 expect(text).toMatch(/ip=\d+\.\d+\.\d+\.\d+/);
55 expect(text).toMatch(/mac=[0-9a-f]{2}:/);
60 }); 56 });
61 57
62 test('/usage returns -1/-1 before auth', async ({ page }) => { 58 test('/usage returns -1/-1 before payment', async ({ page }) => {
63 const resp = await page.goto(`${PORTAL_URL}/usage`); 59 const resp = await page.goto(`${API_URL}/usage`);
64 expect(resp.status()).toBe(200); 60 expect(resp.status()).toBe(200);
65 const text = await resp.text(); 61 const text = await resp.text();
66 expect(text).toBe('-1/-1'); 62 expect(text).toBe('-1/-1');
67 }); 63 });
68 64
69 test('/reset_authentication works', async ({ page }) => { 65 test('API advertisement has correct structure', async ({ page }) => {
70 const resp = await page.goto(`${PORTAL_URL}/reset_authentication`); 66 const resp = await page.goto(API_URL);
71 expect(resp.status()).toBe(200); 67 expect(resp.status()).toBe(200);
72 const data = await resp.json(); 68 const data = await resp.json();
73 expect(data.status).toBe('reset'); 69 expect(data.kind).toBe(10021);
70 expect(data.tags).toBeDefined();
71 expect(data.tags.some(t => t[0] === 'price_per_step')).toBe(true);
72 expect(data.tags.some(t => t[0] === 'step_size')).toBe(true);
73 });
74
75 test('invalid token returns error', async ({ request }) => {
76 const resp = await request.fetch(API_URL, {
77 method: 'POST',
78 data: 'garbage_not_a_token'
79 });
80 expect(resp.status()).toBe(400);
81 const data = await resp.json();
82 expect(data.kind).toBe(21023);
74 }); 83 });
75}); 84});