diff options
Diffstat (limited to 'tests/helpers')
| -rw-r--r-- | tests/helpers/network.mjs | 89 | ||||
| -rw-r--r-- | tests/helpers/serial.mjs | 82 |
2 files changed, 171 insertions, 0 deletions
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 | } | ||