diff options
| author | Your Name <you@example.com> | 2026-05-17 17:18:43 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-17 17:18:43 +0530 |
| commit | 8071741815f0b0938701e80a63e80b0ec94b2778 (patch) | |
| tree | 2a1511480e0b58f4efb144aa9d10c9fba5eed034 /tests/e2e/captive-portal.spec.mjs | |
| parent | 0c2c67b463d6a90aaa0bb69bf3c91dba1d9ec3ec (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/e2e/captive-portal.spec.mjs')
| -rw-r--r-- | tests/e2e/captive-portal.spec.mjs | 118 |
1 files changed, 118 insertions, 0 deletions
diff --git a/tests/e2e/captive-portal.spec.mjs b/tests/e2e/captive-portal.spec.mjs new file mode 100644 index 0000000..ab9d4f1 --- /dev/null +++ b/tests/e2e/captive-portal.spec.mjs | |||
| @@ -0,0 +1,118 @@ | |||
| 1 | import { test, expect } from '@playwright/test'; | ||
| 2 | |||
| 3 | const PORTAL_IP = process.env.TOLLGATE_IP || '10.192.45.1'; | ||
| 4 | const PORTAL_URL = `http://${PORTAL_IP}`; | ||
| 5 | const API_URL = `http://${PORTAL_IP}:2121`; | ||
| 6 | |||
| 7 | test.describe('Captive Portal - Phase 2', () => { | ||
| 8 | |||
| 9 | test('portal page loads with TollGate branding', async ({ page }) => { | ||
| 10 | await page.goto(PORTAL_URL); | ||
| 11 | await expect(page.locator('h1')).toHaveText('TollGate'); | ||
| 12 | await expect(page.locator('.subtitle')).toContainText('internet access'); | ||
| 13 | }); | ||
| 14 | |||
| 15 | test('portal shows price from API', async ({ page }) => { | ||
| 16 | await page.goto(PORTAL_URL); | ||
| 17 | const priceEl = page.locator('.price-amount'); | ||
| 18 | await expect(priceEl).toHaveText(/\d+/, { timeout: 5000 }); | ||
| 19 | }); | ||
| 20 | |||
| 21 | test('portal embeds mint URL without JavaScript fetch', async ({ request }) => { | ||
| 22 | const resp = await request.fetch(PORTAL_URL); | ||
| 23 | const body = await resp.text(); | ||
| 24 | expect(body).not.toContain('Loading...'); | ||
| 25 | expect(body).not.toContain('Error loading mint URL'); | ||
| 26 | expect(body).toMatch(/testnut\.cashu\.space/); | ||
| 27 | }); | ||
| 28 | |||
| 29 | test('portal embeds price without JavaScript fetch', async ({ request }) => { | ||
| 30 | const resp = await request.fetch(PORTAL_URL); | ||
| 31 | const body = await resp.text(); | ||
| 32 | expect(body).not.toContain('__PRICE__'); | ||
| 33 | expect(body).toMatch(/price-amount['"]>\d+</); | ||
| 34 | }); | ||
| 35 | |||
| 36 | test('portal HTML has no unresolved template placeholders', async ({ request }) => { | ||
| 37 | const resp = await request.fetch(PORTAL_URL); | ||
| 38 | const body = await resp.text(); | ||
| 39 | expect(body).not.toContain('__AP_IP__'); | ||
| 40 | expect(body).not.toContain('__MINT_URL__'); | ||
| 41 | expect(body).not.toContain('__PRICE__'); | ||
| 42 | }); | ||
| 43 | |||
| 44 | test('mints section appears after token input in DOM order', async ({ page }) => { | ||
| 45 | await page.goto(PORTAL_URL); | ||
| 46 | const textarea = page.locator('#tokenInput'); | ||
| 47 | const mintUrl = page.locator('#mintUrl'); | ||
| 48 | await expect(textarea).toBeVisible(); | ||
| 49 | await expect(mintUrl).toBeVisible(); | ||
| 50 | const inputBox = await textarea.boundingBox(); | ||
| 51 | const mintBox = await mintUrl.boundingBox(); | ||
| 52 | expect(mintBox.y).toBeGreaterThan(inputBox.y); | ||
| 53 | }); | ||
| 54 | |||
| 55 | test('portal has Cashu token input', async ({ page }) => { | ||
| 56 | await page.goto(PORTAL_URL); | ||
| 57 | const textarea = page.locator('#tokenInput'); | ||
| 58 | await expect(textarea).toBeVisible(); | ||
| 59 | await expect(textarea).toHaveAttribute('placeholder', /cashuA/); | ||
| 60 | }); | ||
| 61 | |||
| 62 | test('portal has Pay & Connect button', async ({ page }) => { | ||
| 63 | await page.goto(PORTAL_URL); | ||
| 64 | const btn = page.locator('#payBtn'); | ||
| 65 | await expect(btn).toBeVisible(); | ||
| 66 | await expect(btn).toHaveText(/Pay/); | ||
| 67 | }); | ||
| 68 | |||
| 69 | test('captive detection URIs return portal HTML (200)', async ({ request }) => { | ||
| 70 | const uris = ['/generate_204', '/hotspot-detect.html', '/canonical.html', '/success.txt']; | ||
| 71 | for (const uri of uris) { | ||
| 72 | const resp = await request.fetch(`${PORTAL_URL}${uri}`); | ||
| 73 | expect(resp.status()).toBe(200); | ||
| 74 | const body = await resp.text(); | ||
| 75 | expect(body).toContain('TollGate'); | ||
| 76 | } | ||
| 77 | }); | ||
| 78 | |||
| 79 | test('catch-all URIs redirect to portal page', async ({ page }) => { | ||
| 80 | await page.goto(`${PORTAL_URL}/some-random-page`); | ||
| 81 | await expect(page.locator('h1')).toHaveText('TollGate'); | ||
| 82 | }); | ||
| 83 | |||
| 84 | test('/whoami returns ip and mac', async ({ page }) => { | ||
| 85 | const resp = await page.goto(`${API_URL}/whoami`); | ||
| 86 | expect(resp.status()).toBe(200); | ||
| 87 | const text = await resp.text(); | ||
| 88 | expect(text).toMatch(/ip=\d+\.\d+\.\d+\.\d+/); | ||
| 89 | expect(text).toMatch(/mac=(unknown|[0-9a-f]{2}:)/); | ||
| 90 | }); | ||
| 91 | |||
| 92 | test('/usage returns -1/-1 before payment', async ({ page }) => { | ||
| 93 | const resp = await page.goto(`${API_URL}/usage`); | ||
| 94 | expect(resp.status()).toBe(200); | ||
| 95 | const text = await resp.text(); | ||
| 96 | expect(text).toBe('-1/-1'); | ||
| 97 | }); | ||
| 98 | |||
| 99 | test('API advertisement has correct structure', async ({ page }) => { | ||
| 100 | const resp = await page.goto(API_URL); | ||
| 101 | expect(resp.status()).toBe(200); | ||
| 102 | const data = await resp.json(); | ||
| 103 | expect(data.kind).toBe(10021); | ||
| 104 | expect(data.tags).toBeDefined(); | ||
| 105 | expect(data.tags.some(t => t[0] === 'price_per_step')).toBe(true); | ||
| 106 | expect(data.tags.some(t => t[0] === 'step_size')).toBe(true); | ||
| 107 | }); | ||
| 108 | |||
| 109 | test('invalid token returns error', async ({ request }) => { | ||
| 110 | const resp = await request.fetch(API_URL, { | ||
| 111 | method: 'POST', | ||
| 112 | data: 'garbage_not_a_token' | ||
| 113 | }); | ||
| 114 | expect(resp.status()).toBe(400); | ||
| 115 | const data = await resp.json(); | ||
| 116 | expect(data.kind).toBe(21023); | ||
| 117 | }); | ||
| 118 | }); | ||