diff options
| author | Your Name <you@example.com> | 2026-05-16 04:51:57 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-16 04:51:57 +0530 |
| commit | ee4e13680f522253f94e8ebdea5df80332afc495 (patch) | |
| tree | eb3270f5be079067b07de15b9af094c30582c68c /tests | |
| parent | 3063dea143b576792e5831421e5607cbd60d6816 (diff) | |
Phase 2 Playwright tests: 10/10 passing (portal, captive detection, API)
- Updated from Phase 1 tests to Phase 2 (302 redirects, Cashu token input)
- Test captive detection URIs return 302 (using request API)
- Test invalid token via request API (no CORS issues)
- Tests: portal branding, price, token input, pay button, detection redirects
- Tests: whoami, usage, API advertisement, invalid token
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/captive-portal.spec.mjs | 73 |
1 files changed, 41 insertions, 32 deletions
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 | ||
| 3 | const PORTAL_IP = process.env.TOLLGATE_IP || '192.168.4.1'; | 3 | const PORTAL_IP = process.env.TOLLGATE_IP || '192.168.4.1'; |
| 4 | const PORTAL_URL = `http://${PORTAL_IP}`; | 4 | const PORTAL_URL = `http://${PORTAL_IP}`; |
| 5 | const API_URL = `http://${PORTAL_IP}:2121`; | ||
| 5 | 6 | ||
| 6 | test.describe('Captive Portal - Phase 1', () => { | 7 | test.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 | }); |