diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/api.mjs | 79 | ||||
| -rw-r--r-- | tests/captive-portal.spec.mjs | 75 | ||||
| -rw-r--r-- | tests/helpers/network.mjs | 89 | ||||
| -rw-r--r-- | tests/helpers/serial.mjs | 82 | ||||
| -rw-r--r-- | tests/network.mjs | 66 | ||||
| -rw-r--r-- | tests/playwright.config.mjs | 16 | ||||
| -rw-r--r-- | tests/smoke.mjs | 52 |
7 files changed, 459 insertions, 0 deletions
diff --git a/tests/api.mjs b/tests/api.mjs new file mode 100644 index 0000000..5218d7b --- /dev/null +++ b/tests/api.mjs | |||
| @@ -0,0 +1,79 @@ | |||
| 1 | import { curl, curlBody, getPortalIP, canPing, canResolve, dnsResolvesToSelf } from './helpers/network.mjs'; | ||
| 2 | |||
| 3 | const IP = getPortalIP(); | ||
| 4 | let passed = 0, failed = 0; | ||
| 5 | |||
| 6 | function assert(condition, test) { | ||
| 7 | if (condition) { console.log(` ✓ ${test}`); passed++; } | ||
| 8 | else { console.log(` ✗ ${test}`); failed++; } | ||
| 9 | } | ||
| 10 | |||
| 11 | async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } | ||
| 12 | |||
| 13 | console.log(`\n=== API Tests (target: ${IP}) ===\n`); | ||
| 14 | |||
| 15 | // Test 3: Captive portal serves HTML | ||
| 16 | console.log('Test 3: GET / returns portal HTML'); | ||
| 17 | const body3 = curlBody(`http://${IP}/`); | ||
| 18 | assert(body3 && body3.includes('TollGate'), 'Portal HTML contains "TollGate"'); | ||
| 19 | assert(body3 && body3.includes('Grant Free Access'), 'Portal has Grant Access button'); | ||
| 20 | |||
| 21 | // Test 4: Captive detection URIs | ||
| 22 | console.log('\nTest 4: Captive detection URIs'); | ||
| 23 | for (const uri of ['/generate_204', '/hotspot-detect.html', '/canonical.html', '/success.txt', '/ncsi.txt', '/connecttest.txt', '/wpad.dat', '/redirect']) { | ||
| 24 | const code = curl(`http://${IP}${uri}`); | ||
| 25 | assert(code === '200', `${uri} → 200`); | ||
| 26 | } | ||
| 27 | |||
| 28 | // Test 7: /whoami returns MAC | ||
| 29 | console.log('\nTest 7: GET /whoami'); | ||
| 30 | const body7 = curlBody(`http://${IP}/whoami`); | ||
| 31 | assert(body7 && body7.startsWith('mac='), '/whoami returns mac=...'); | ||
| 32 | |||
| 33 | // Test 8: /usage returns no session | ||
| 34 | console.log('\nTest 8: GET /usage'); | ||
| 35 | const body8 = curlBody(`http://${IP}/usage`); | ||
| 36 | assert(body8 && body8.includes('-1/-1'), '/usage returns -1/-1 before auth'); | ||
| 37 | |||
| 38 | // Test 5: DNS hijack before auth | ||
| 39 | console.log('\nTest 5: DNS hijack before auth'); | ||
| 40 | assert(dnsResolvesToSelf('google.com'), 'DNS resolves google.com to AP IP'); | ||
| 41 | |||
| 42 | // Test 6: No internet before auth | ||
| 43 | console.log('\nTest 6: No internet before auth'); | ||
| 44 | assert(!canPing('8.8.8.8', 1), 'ping 8.8.8.8 fails before auth'); | ||
| 45 | |||
| 46 | // Test 9: Grant access | ||
| 47 | console.log('\nTest 9: GET /grant_access'); | ||
| 48 | const body9 = curlBody(`http://${IP}/grant_access`); | ||
| 49 | assert(body9 && body9.includes('"granted"'), 'Grant access returns {"status":"granted"}'); | ||
| 50 | |||
| 51 | await sleep(2000); | ||
| 52 | |||
| 53 | // Test 10: DNS forward after auth | ||
| 54 | console.log('\nTest 10: DNS forward after auth'); | ||
| 55 | assert(canResolve('google.com'), 'DNS resolves normally after auth'); | ||
| 56 | |||
| 57 | // Test 11: Internet after auth | ||
| 58 | console.log('\nTest 11: Internet after auth'); | ||
| 59 | assert(canPing('8.8.8.8'), 'ping 8.8.8.8 succeeds after auth'); | ||
| 60 | |||
| 61 | // Test 12: HTTP browsing works | ||
| 62 | console.log('\nTest 12: HTTP browsing'); | ||
| 63 | const body12 = curlBody('http://example.com/'); | ||
| 64 | assert(body12 && (body12.includes('Example Domain') || body12.includes('example')), 'HTTP page loads'); | ||
| 65 | |||
| 66 | // Test 13: Reset auth | ||
| 67 | console.log('\nTest 13: GET /reset_authentication'); | ||
| 68 | const body13 = curlBody(`http://${IP}/reset_authentication`); | ||
| 69 | assert(body13 && body13.includes('"reset"'), 'Reset returns {"status":"reset"}'); | ||
| 70 | |||
| 71 | await sleep(2000); | ||
| 72 | |||
| 73 | // Test 14: Internet blocked after reset | ||
| 74 | console.log('\nTest 14: Internet blocked after reset'); | ||
| 75 | assert(!canPing('8.8.8.8', 1), 'ping fails after auth reset'); | ||
| 76 | |||
| 77 | // Summary | ||
| 78 | console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`); | ||
| 79 | process.exit(failed > 0 ? 1 : 0); | ||
diff --git a/tests/captive-portal.spec.mjs b/tests/captive-portal.spec.mjs new file mode 100644 index 0000000..b6ad96b --- /dev/null +++ b/tests/captive-portal.spec.mjs | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | import { test, expect } from '@playwright/test'; | ||
| 2 | |||
| 3 | const PORTAL_IP = process.env.TOLLGATE_IP || '192.168.4.1'; | ||
| 4 | const PORTAL_URL = `http://${PORTAL_IP}`; | ||
| 5 | |||
| 6 | test.describe('Captive Portal - Phase 1', () => { | ||
| 7 | |||
| 8 | test('portal page loads with TollGate branding', async ({ page }) => { | ||
| 9 | await page.goto(PORTAL_URL); | ||
| 10 | await expect(page.locator('h1')).toHaveText('TollGate'); | ||
| 11 | await expect(page.locator('.subtitle')).toContainText('internet access'); | ||
| 12 | }); | ||
| 13 | |||
| 14 | test('portal shows price', async ({ page }) => { | ||
| 15 | await page.goto(PORTAL_URL); | ||
| 16 | const priceEl = page.locator('.price-amount'); | ||
| 17 | await expect(priceEl).not.toBeEmpty({ timeout: 5000 }); | ||
| 18 | }); | ||
| 19 | |||
| 20 | test('grant access button exists', async ({ page }) => { | ||
| 21 | await page.goto(PORTAL_URL); | ||
| 22 | const btn = page.locator('#grantBtn'); | ||
| 23 | await expect(btn).toBeVisible(); | ||
| 24 | await expect(btn).toHaveText(/Grant Free Access/i); | ||
| 25 | }); | ||
| 26 | |||
| 27 | test('click grant access shows connected', async ({ page }) => { | ||
| 28 | await page.goto(PORTAL_URL); | ||
| 29 | const btn = page.locator('#grantBtn'); | ||
| 30 | await btn.click(); | ||
| 31 | const status = page.locator('#status.success'); | ||
| 32 | await expect(status).toBeVisible({ timeout: 10000 }); | ||
| 33 | await expect(status).toContainText(/Connected/i); | ||
| 34 | }); | ||
| 35 | |||
| 36 | test('captive detection URIs return portal', async ({ page }) => { | ||
| 37 | const uris = ['/generate_204', '/hotspot-detect.html', '/canonical.html', '/success.txt']; | ||
| 38 | for (const uri of uris) { | ||
| 39 | const resp = await page.goto(`${PORTAL_URL}${uri}`); | ||
| 40 | expect(resp.status()).toBe(200); | ||
| 41 | const body = await resp.text(); | ||
| 42 | expect(body).toContain('TollGate'); | ||
| 43 | } | ||
| 44 | }); | ||
| 45 | |||
| 46 | test('/api/status returns JSON with price', async ({ page }) => { | ||
| 47 | const resp = await page.goto(`${PORTAL_URL}/api/status`); | ||
| 48 | expect(resp.status()).toBe(200); | ||
| 49 | const data = await resp.json(); | ||
| 50 | expect(data).toHaveProperty('connected'); | ||
| 51 | expect(data).toHaveProperty('price'); | ||
| 52 | expect(typeof data.price).toBe('number'); | ||
| 53 | }); | ||
| 54 | |||
| 55 | test('/whoami returns mac address', async ({ page }) => { | ||
| 56 | const resp = await page.goto(`${PORTAL_URL}/whoami`); | ||
| 57 | expect(resp.status()).toBe(200); | ||
| 58 | const text = await resp.text(); | ||
| 59 | expect(text).toMatch(/^mac=/); | ||
| 60 | }); | ||
| 61 | |||
| 62 | test('/usage returns -1/-1 before auth', async ({ page }) => { | ||
| 63 | const resp = await page.goto(`${PORTAL_URL}/usage`); | ||
| 64 | expect(resp.status()).toBe(200); | ||
| 65 | const text = await resp.text(); | ||
| 66 | expect(text).toBe('-1/-1'); | ||
| 67 | }); | ||
| 68 | |||
| 69 | test('/reset_authentication works', async ({ page }) => { | ||
| 70 | const resp = await page.goto(`${PORTAL_URL}/reset_authentication`); | ||
| 71 | expect(resp.status()).toBe(200); | ||
| 72 | const data = await resp.json(); | ||
| 73 | expect(data.status).toBe('reset'); | ||
| 74 | }); | ||
| 75 | }); | ||
diff --git a/tests/helpers/network.mjs b/tests/helpers/network.mjs new file mode 100644 index 0000000..e4d5086 --- /dev/null +++ b/tests/helpers/network.mjs | |||
| @@ -0,0 +1,89 @@ | |||
| 1 | import { execSync } from 'child_process'; | ||
| 2 | |||
| 3 | const ESP32_IP = process.env.TOLLGATE_IP || '192.168.4.1'; | ||
| 4 | const TIMEOUT = 5000; | ||
| 5 | |||
| 6 | export function curl(args, expectStatus = null) { | ||
| 7 | const cmd = `curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 --max-time ${TIMEOUT/1000} ${args}`; | ||
| 8 | try { | ||
| 9 | const result = execSync(cmd, { encoding: 'utf8', timeout: TIMEOUT + 2000 }).trim(); | ||
| 10 | if (expectStatus && result !== String(expectStatus)) { | ||
| 11 | throw new Error(`Expected HTTP ${expectStatus}, got ${result}`); | ||
| 12 | } | ||
| 13 | return result; | ||
| 14 | } catch (e) { | ||
| 15 | if (e.status === 'ETIMEDOUT' || e.killed) return 'TIMEOUT'; | ||
| 16 | throw e; | ||
| 17 | } | ||
| 18 | } | ||
| 19 | |||
| 20 | export function curlBody(url) { | ||
| 21 | const cmd = `curl -s --connect-timeout 5 --max-time ${TIMEOUT/1000} "${url}"`; | ||
| 22 | try { | ||
| 23 | return execSync(cmd, { encoding: 'utf8', timeout: TIMEOUT + 2000 }); | ||
| 24 | } catch { | ||
| 25 | return null; | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | export function getPortalIP() { return ESP32_IP; } | ||
| 30 | |||
| 31 | export function canPing(host = '8.8.8.8', count = 2) { | ||
| 32 | try { | ||
| 33 | const result = execSync(`ping -c ${count} -W 2 -I wlp59s0 ${host}`, { encoding: 'utf8', timeout: 10000 }); | ||
| 34 | return result.includes('0% packet loss') || result.includes('1 packets transmitted'); | ||
| 35 | } catch { | ||
| 36 | return false; | ||
| 37 | } | ||
| 38 | } | ||
| 39 | |||
| 40 | export function canResolve(domain = 'google.com') { | ||
| 41 | try { | ||
| 42 | const result = execSync(`nslookup ${domain} ${ESP32_IP}`, { encoding: 'utf8', timeout: 10000 }); | ||
| 43 | return result.includes('Address') && !result.includes('NXDOMAIN'); | ||
| 44 | } catch (e) { | ||
| 45 | const result = e.stdout || ''; | ||
| 46 | return result.includes('Address') && !result.includes('NXDOMAIN'); | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | export function dnsResolvesToSelf(domain = 'google.com') { | ||
| 51 | try { | ||
| 52 | const result = execSync(`nslookup ${domain} ${ESP32_IP}`, { encoding: 'utf8', timeout: 10000 }); | ||
| 53 | return result.includes(ESP32_IP); | ||
| 54 | } catch (e) { | ||
| 55 | return e.stdout && e.stdout.includes(ESP32_IP); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | export function connectToAP(ssid, password = '') { | ||
| 60 | try { | ||
| 61 | if (password) { | ||
| 62 | execSync(`nmcli dev wifi connect "${ssid}" password "${password}" ifname wlan0`, { timeout: 30000 }); | ||
| 63 | } else { | ||
| 64 | execSync(`nmcli dev wifi connect "${ssid}" ifname wlan0`, { timeout: 30000 }); | ||
| 65 | } | ||
| 66 | return true; | ||
| 67 | } catch { | ||
| 68 | return false; | ||
| 69 | } | ||
| 70 | } | ||
| 71 | |||
| 72 | export function disconnectAP() { | ||
| 73 | try { | ||
| 74 | execSync('nmcli dev disconnect wlan0 2>/dev/null || true', { timeout: 10000 }); | ||
| 75 | return true; | ||
| 76 | } catch { | ||
| 77 | return false; | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | export function getWifiInterface() { | ||
| 82 | try { | ||
| 83 | const result = execSync('nmcli -t -f DEVICE,TYPE dev status', { encoding: 'utf8' }); | ||
| 84 | const line = result.split('\n').find(l => l.includes('wifi')); | ||
| 85 | return line ? line.split(':')[0] : null; | ||
| 86 | } catch { | ||
| 87 | return null; | ||
| 88 | } | ||
| 89 | } | ||
diff --git a/tests/helpers/serial.mjs b/tests/helpers/serial.mjs new file mode 100644 index 0000000..306b552 --- /dev/null +++ b/tests/helpers/serial.mjs | |||
| @@ -0,0 +1,82 @@ | |||
| 1 | import { SerialPort } from 'serialport'; | ||
| 2 | import { ReadlineParser } from '@serialport/parser-readline'; | ||
| 3 | import { execSync } from 'child_process'; | ||
| 4 | |||
| 5 | const DEFAULT_BAUD = 115200; | ||
| 6 | const BOOT_TIMEOUT = 30000; | ||
| 7 | |||
| 8 | export async function execSerial(portPath, command, timeoutMs = 5000) { | ||
| 9 | return new Promise((resolve, reject) => { | ||
| 10 | const port = new SerialPort({ path: portPath, baudRate: DEFAULT_BAUD }); | ||
| 11 | const parser = port.pipe(new ReadlineParser()); | ||
| 12 | const lines = []; | ||
| 13 | let resolved = false; | ||
| 14 | |||
| 15 | const timer = setTimeout(() => { | ||
| 16 | if (!resolved) { resolved = true; port.close(); resolve(lines.join('\n')); } | ||
| 17 | }, timeoutMs); | ||
| 18 | |||
| 19 | parser.on('data', (line) => { | ||
| 20 | lines.push(line); | ||
| 21 | if (line.includes('___END___') && !resolved) { | ||
| 22 | resolved = true; | ||
| 23 | clearTimeout(timer); | ||
| 24 | port.close(); | ||
| 25 | resolve(lines.join('\n')); | ||
| 26 | } | ||
| 27 | }); | ||
| 28 | |||
| 29 | port.on('open', () => { | ||
| 30 | port.write(command + '\n'); | ||
| 31 | }); | ||
| 32 | |||
| 33 | port.on('error', (err) => { | ||
| 34 | if (!resolved) { resolved = true; clearTimeout(timer); reject(err); } | ||
| 35 | }); | ||
| 36 | }); | ||
| 37 | } | ||
| 38 | |||
| 39 | export async function waitForBoot(portPath, timeoutMs = BOOT_TIMEOUT) { | ||
| 40 | return new Promise((resolve, reject) => { | ||
| 41 | const port = new SerialPort({ path: portPath, baudRate: DEFAULT_BAUD }); | ||
| 42 | const parser = port.pipe(new ReadlineParser()); | ||
| 43 | const timer = setTimeout(() => { | ||
| 44 | port.close(); | ||
| 45 | reject(new Error('Boot timeout')); | ||
| 46 | }, timeoutMs); | ||
| 47 | |||
| 48 | parser.on('data', (line) => { | ||
| 49 | if (line.includes('TollGate services started') || line.includes('WiFi AP+STA started')) { | ||
| 50 | clearTimeout(timer); | ||
| 51 | setTimeout(() => { port.close(); resolve(true); }, 500); | ||
| 52 | } | ||
| 53 | }); | ||
| 54 | |||
| 55 | port.on('error', (err) => { | ||
| 56 | clearTimeout(timer); | ||
| 57 | reject(err); | ||
| 58 | }); | ||
| 59 | }); | ||
| 60 | } | ||
| 61 | |||
| 62 | export async function readSerial(portPath, durationMs = 3000) { | ||
| 63 | return new Promise((resolve, reject) => { | ||
| 64 | const port = new SerialPort({ path: portPath, baudRate: DEFAULT_BAUD }); | ||
| 65 | const parser = port.pipe(new ReadlineParser()); | ||
| 66 | const lines = []; | ||
| 67 | |||
| 68 | const timer = setTimeout(() => { | ||
| 69 | port.close(); | ||
| 70 | resolve(lines.join('\n')); | ||
| 71 | }, durationMs); | ||
| 72 | |||
| 73 | parser.on('data', (line) => lines.push(line)); | ||
| 74 | port.on('error', (err) => { clearTimeout(timer); reject(err); }); | ||
| 75 | }); | ||
| 76 | } | ||
| 77 | |||
| 78 | export function resetDevice(portPath) { | ||
| 79 | try { | ||
| 80 | execSync(`python3 -m esptool --port ${portPath} run 2>/dev/null`, { timeout: 5000 }); | ||
| 81 | } catch {} | ||
| 82 | } | ||
diff --git a/tests/network.mjs b/tests/network.mjs new file mode 100644 index 0000000..2d302ef --- /dev/null +++ b/tests/network.mjs | |||
| @@ -0,0 +1,66 @@ | |||
| 1 | import { execSync } from 'child_process'; | ||
| 2 | |||
| 3 | const IP = process.env.TOLLGATE_IP || '192.168.4.1'; | ||
| 4 | let passed = 0, failed = 0; | ||
| 5 | |||
| 6 | function assert(condition, test) { | ||
| 7 | if (condition) { console.log(` ✓ ${test}`); passed++; } | ||
| 8 | else { console.log(` ✗ ${test}`); failed++; } | ||
| 9 | } | ||
| 10 | |||
| 11 | function run(cmd) { | ||
| 12 | try { return execSync(cmd, { encoding: 'utf8', timeout: 15000 }); } | ||
| 13 | catch { return null; } | ||
| 14 | } | ||
| 15 | |||
| 16 | console.log(`\n=== Network Tests (target: ${IP}) ===\n`); | ||
| 17 | |||
| 18 | // Test 1: AP visible in scan | ||
| 19 | console.log('Test 1: AP visible in scan'); | ||
| 20 | const scan = run('nmcli -t -f SSID dev wifi list 2>/dev/null'); | ||
| 21 | assert(scan && scan.includes('TollGate'), 'TollGate SSID visible in WiFi scan'); | ||
| 22 | |||
| 23 | // Test 2: DHCP lease | ||
| 24 | console.log('\nTest 2: DHCP lease / connectivity'); | ||
| 25 | const ip_show = run(`ip addr show | grep "inet ${IP.split('.').slice(0,3).join('.')}"`); | ||
| 26 | assert(ip_show !== null, `Has IP in ${IP.split('.').slice(0,3).join('.')}.* subnet`); | ||
| 27 | |||
| 28 | // Test 5: DNS hijack | ||
| 29 | console.log('\nTest 5: DNS hijack before auth'); | ||
| 30 | const ns1 = run(`nslookup random-test.example.com ${IP} 2>/dev/null`); | ||
| 31 | assert(ns1 && ns1.includes(IP), 'DNS resolves arbitrary domain to AP IP'); | ||
| 32 | |||
| 33 | // Test 6: No internet | ||
| 34 | console.log('\nTest 6: No internet before auth'); | ||
| 35 | const ping1 = run('ping -c 1 -W 3 1.1.1.1 2>/dev/null'); | ||
| 36 | assert(ping1 === null || ping1.includes('100% packet loss'), 'Internet blocked before auth'); | ||
| 37 | |||
| 38 | // Grant access for further tests | ||
| 39 | console.log('\nGranting access...'); | ||
| 40 | run(`curl -s http://${IP}/grant_access`); | ||
| 41 | |||
| 42 | import { execSync as exec } from 'child_process'; | ||
| 43 | await new Promise(r => setTimeout(r, 2000)); | ||
| 44 | |||
| 45 | // Test 10: DNS forward | ||
| 46 | console.log('Test 10: DNS forward after auth'); | ||
| 47 | const ns2 = run(`nslookup google.com ${IP} 2>/dev/null`); | ||
| 48 | assert(ns2 && !ns2.includes(IP) && ns2.includes('Address'), 'DNS resolves to real IPs'); | ||
| 49 | |||
| 50 | // Test 11: Internet | ||
| 51 | console.log('\nTest 11: Internet after auth'); | ||
| 52 | const ping2 = run('ping -c 2 -W 3 8.8.8.8'); | ||
| 53 | assert(ping2 && !ping2.includes('100% packet loss'), 'ping succeeds after auth'); | ||
| 54 | |||
| 55 | // Reset | ||
| 56 | console.log('\nResetting auth...'); | ||
| 57 | run(`curl -s http://${IP}/reset_authentication`); | ||
| 58 | await new Promise(r => setTimeout(r, 2000)); | ||
| 59 | |||
| 60 | // Test 14 | ||
| 61 | console.log('Test 14: Internet blocked after reset'); | ||
| 62 | const ping3 = run('ping -c 1 -W 3 8.8.8.8 2>/dev/null'); | ||
| 63 | assert(ping3 === null || ping3.includes('100% packet loss'), 'Internet blocked after reset'); | ||
| 64 | |||
| 65 | console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`); | ||
| 66 | process.exit(failed > 0 ? 1 : 0); | ||
diff --git a/tests/playwright.config.mjs b/tests/playwright.config.mjs new file mode 100644 index 0000000..fee0815 --- /dev/null +++ b/tests/playwright.config.mjs | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | import { defineConfig } from '@playwright/test'; | ||
| 2 | |||
| 3 | export default defineConfig({ | ||
| 4 | testDir: '.', | ||
| 5 | testMatch: '*.spec.mjs', | ||
| 6 | timeout: 60000, | ||
| 7 | retries: 0, | ||
| 8 | use: { | ||
| 9 | headless: true, | ||
| 10 | viewport: { width: 1280, height: 900 }, | ||
| 11 | screenshot: 'only-on-failure', | ||
| 12 | trace: 'on-first-retry', | ||
| 13 | }, | ||
| 14 | reporter: [['list'], ['html', { open: 'never' }]], | ||
| 15 | workers: 1, | ||
| 16 | }); | ||
diff --git a/tests/smoke.mjs b/tests/smoke.mjs new file mode 100644 index 0000000..19f96de --- /dev/null +++ b/tests/smoke.mjs | |||
| @@ -0,0 +1,52 @@ | |||
| 1 | import { execSync } from 'child_process'; | ||
| 2 | |||
| 3 | const PORT = process.argv[2] || '/dev/ttyACM0'; | ||
| 4 | const IP = process.env.TOLLGATE_IP || '192.168.4.1'; | ||
| 5 | const SSID = process.env.AP_SSID || 'TollGate'; | ||
| 6 | |||
| 7 | console.log(`\n=== Smoke Test (30s) ===`); | ||
| 8 | console.log(`Port: ${PORT}, Portal IP: ${IP}, SSID: ${SSID}\n`); | ||
| 9 | |||
| 10 | let passed = 0, failed = 0; | ||
| 11 | function assert(cond, msg) { | ||
| 12 | if (cond) { console.log(` ✓ ${msg}`); passed++; } | ||
| 13 | else { console.log(` ✗ ${msg}`); failed++; } | ||
| 14 | } | ||
| 15 | |||
| 16 | function run(cmd) { | ||
| 17 | try { return execSync(cmd, { encoding: 'utf8', timeout: 10000 }); } | ||
| 18 | catch { return null; } | ||
| 19 | } | ||
| 20 | |||
| 21 | // 1. Check AP visible | ||
| 22 | const scan = run('nmcli -t -f SSID dev wifi list 2>/dev/null'); | ||
| 23 | assert(scan && scan.includes(SSID), `SSID "${SSID}" visible`); | ||
| 24 | |||
| 25 | // 2. Check we can reach portal | ||
| 26 | const portal = run(`curl -s --connect-timeout 5 http://${IP}/`); | ||
| 27 | assert(portal && portal.includes('TollGate'), 'Portal HTML loads'); | ||
| 28 | |||
| 29 | // 3. Grant access | ||
| 30 | const grant = run(`curl -s http://${IP}/grant_access`); | ||
| 31 | assert(grant && grant.includes('granted'), 'Grant access works'); | ||
| 32 | |||
| 33 | // Wait for DNS | ||
| 34 | const sleep = ms => new Promise(r => setTimeout(r, ms)); | ||
| 35 | await sleep(2000); | ||
| 36 | |||
| 37 | // 4. Internet works | ||
| 38 | const ping = run('ping -c 1 -W 3 -I wlp59s0 1.1.1.1 2>/dev/null'); | ||
| 39 | assert(ping && !ping.includes('100% packet loss'), 'Internet works after grant'); | ||
| 40 | |||
| 41 | // 5. Reset | ||
| 42 | const reset = run(`curl -s http://${IP}/reset_authentication`); | ||
| 43 | assert(reset && reset.includes('reset'), 'Reset auth works'); | ||
| 44 | |||
| 45 | await sleep(2000); | ||
| 46 | |||
| 47 | // 6. Internet blocked | ||
| 48 | const ping2 = run('ping -c 1 -W 3 -I wlp59s0 1.1.1.1 2>/dev/null'); | ||
| 49 | assert(!ping2 || ping2.includes('100% packet loss'), 'Internet blocked after reset'); | ||
| 50 | |||
| 51 | console.log(`\n=== Smoke: ${passed} passed, ${failed} failed ===\n`); | ||
| 52 | process.exit(failed > 0 ? 1 : 0); | ||