mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-16 07:51:36 +10:00
test: harden portal hardware contract
This commit is contained in:
@@ -74,6 +74,11 @@ scan, a missing-route response, and desktop/mobile portal rendering with no
|
||||
browser page errors. Screenshots, traces on failure, JSON results, and the HTML
|
||||
report are saved under the printed XDG state-directory artifact path.
|
||||
|
||||
On ESP8266, an AP+STA scan can briefly move the radio off the AP channel. The
|
||||
client may reconnect during that interval; the contract deliberately retries
|
||||
that transport interruption and still requires a reachable portal with a
|
||||
complete, valid scan result.
|
||||
|
||||
For interactive diagnosis, leave the temporary client connection up and remove
|
||||
only that managed connection when finished:
|
||||
|
||||
@@ -88,6 +93,10 @@ the fixture to a real LAN. Copy the ignored template below, add local
|
||||
credentials, and pass it explicitly; it is mounted read-only into the test
|
||||
container and is never logged by the runner.
|
||||
|
||||
A retained session is deliberately never overwritten. If a previous `up` or an
|
||||
interrupted `run` left one behind, run `./tools/portal-hardware down` first;
|
||||
that removes only the named temporary connection recorded by the tool.
|
||||
|
||||
```bash
|
||||
cp test/portal-station.env.example test/portal-station.env
|
||||
./tools/portal-hardware run ... --station-env test/portal-station.env
|
||||
|
||||
@@ -5,13 +5,12 @@ This test fixture starts only the WiFiManager captive portal. It intentionally h
|
||||
Use it through the repository runner so a secondary Wi-Fi adapter is explicitly selected and protected from becoming the host default route:
|
||||
|
||||
```bash
|
||||
./tools/test-portal-browser.sh \
|
||||
./tools/portal-hardware run \
|
||||
--platform esp8266 \
|
||||
--port /dev/serial/by-id/... \
|
||||
--wifi-interface wlx... \
|
||||
--output /tmp/wifimanager-browser
|
||||
--client-interface wlx...
|
||||
```
|
||||
|
||||
The ESP8266 portal SSID is `WM Browser ESP8266`; the ESP32 SSID is `WM Browser ESP32`. Both use `default1` exclusively for local development tests.
|
||||
The ESP8266 portal SSID is `WM Contract ESP8266`; the ESP32 SSID is `WM Contract ESP32`. Both use `default1` exclusively for local development tests.
|
||||
|
||||
The runner cleans up only the temporary connection it creates on the named secondary interface. It refuses to run if that interface is the system default route.
|
||||
|
||||
@@ -7,10 +7,18 @@ async function json(response) {
|
||||
async function waitForScan(request) {
|
||||
let result;
|
||||
await expect.poll(async () => {
|
||||
const response = await request.get('/api/wifi/scan-status');
|
||||
expect(response.ok()).toBeTruthy();
|
||||
result = await json(response);
|
||||
return result.scanning;
|
||||
try {
|
||||
const response = await request.get('/api/wifi/scan-status');
|
||||
if (!response.ok()) return true;
|
||||
result = await json(response);
|
||||
return result.scanning;
|
||||
} catch {
|
||||
// ESP8266 AP+STA scans briefly leave the AP channel. A client can lose
|
||||
// its association while the radio scans, then reconnect before the
|
||||
// asynchronous scan completes. Keep polling; the assertions below still
|
||||
// require a reachable portal with a complete, valid result.
|
||||
return true;
|
||||
}
|
||||
}, { timeout: 45_000, intervals: [500, 800, 1_000] }).toBe(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -122,27 +122,56 @@ wm_state_file() {
|
||||
printf '%s/session.env\n' "$(wm_portal_state_root)"
|
||||
}
|
||||
|
||||
wm_require_no_active_session() {
|
||||
local file
|
||||
file="$(wm_state_file)"
|
||||
[[ ! -e "$file" ]] || {
|
||||
echo "An existing WiFiManager portal session is recorded; run ./tools/portal-hardware down first." >&2
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
wm_write_state() {
|
||||
local interface="$1" platform="$2" uuid="$3" name="$4" root file
|
||||
root="$(wm_portal_state_root)"
|
||||
file="$(wm_state_file)"
|
||||
install -d -m 700 "$root"
|
||||
umask 077
|
||||
printf 'WM_PORTAL_INTERFACE=%q\nWM_PORTAL_PLATFORM=%q\nWM_PORTAL_CONNECTION_UUID=%q\nWM_PORTAL_CONNECTION_NAME=%q\n' \
|
||||
"$interface" "$platform" "$uuid" "$name" >"$file"
|
||||
(
|
||||
umask 077
|
||||
printf 'WM_PORTAL_INTERFACE=%s\nWM_PORTAL_PLATFORM=%s\nWM_PORTAL_CONNECTION_UUID=%s\nWM_PORTAL_CONNECTION_NAME=%s\n' \
|
||||
"$interface" "$platform" "$uuid" "$name" >"$file"
|
||||
)
|
||||
chmod 600 "$file"
|
||||
}
|
||||
|
||||
wm_load_state() {
|
||||
local file
|
||||
local file key value
|
||||
file="$(wm_state_file)"
|
||||
[[ -f "$file" ]] || {
|
||||
echo "No active WiFiManager portal session was found." >&2
|
||||
return 1
|
||||
}
|
||||
# The state file is created above using shell-escaped values and mode 0600.
|
||||
# shellcheck disable=SC1090
|
||||
source "$file"
|
||||
WM_PORTAL_INTERFACE=""
|
||||
WM_PORTAL_PLATFORM=""
|
||||
WM_PORTAL_CONNECTION_UUID=""
|
||||
WM_PORTAL_CONNECTION_NAME=""
|
||||
while IFS='=' read -r key value; do
|
||||
case "$key" in
|
||||
WM_PORTAL_INTERFACE|WM_PORTAL_PLATFORM|WM_PORTAL_CONNECTION_UUID|WM_PORTAL_CONNECTION_NAME)
|
||||
printf -v "$key" '%s' "$value"
|
||||
;;
|
||||
'') ;;
|
||||
*)
|
||||
echo "Invalid WiFiManager portal session state." >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done <"$file"
|
||||
[[ -n "$WM_PORTAL_INTERFACE" && -n "$WM_PORTAL_PLATFORM" && -n "$WM_PORTAL_CONNECTION_UUID" && -n "$WM_PORTAL_CONNECTION_NAME" ]] || {
|
||||
echo "Incomplete WiFiManager portal session state." >&2
|
||||
return 1
|
||||
}
|
||||
export WM_PORTAL_INTERFACE WM_PORTAL_PLATFORM WM_PORTAL_CONNECTION_UUID WM_PORTAL_CONNECTION_NAME
|
||||
}
|
||||
|
||||
wm_clear_state() {
|
||||
|
||||
@@ -13,7 +13,7 @@ Usage:
|
||||
--client-interface IFACE [--take-over-client-adapter] [--output DIRECTORY]
|
||||
./tools/portal-hardware run --platform esp8266|esp32 --port /dev/serial/by-id/... \
|
||||
--client-interface IFACE [--take-over-client-adapter] [--keep] \
|
||||
[--browser auto|require|skip] [--station-env PATH] [--output DIRECTORY]
|
||||
[--browser auto|skip] [--station-env PATH] [--output DIRECTORY]
|
||||
./tools/portal-hardware down
|
||||
|
||||
Only the named client interface may be disconnected or reconfigured. The tool
|
||||
@@ -72,7 +72,7 @@ validate_run_arguments() {
|
||||
echo "Serial port not found: $port" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$browser" == "auto" || "$browser" == "require" || "$browser" == "skip" ]] || usage
|
||||
[[ "$browser" == "auto" || "$browser" == "skip" ]] || usage
|
||||
[[ -z "$station_env" || -r "$station_env" ]] || {
|
||||
echo "Station environment file is not readable: $station_env" >&2
|
||||
exit 1
|
||||
@@ -84,6 +84,7 @@ start_portal_session() {
|
||||
validate_run_arguments
|
||||
require_common
|
||||
wm_acquire_hardware_lock
|
||||
wm_require_no_active_session
|
||||
wm_require_client_adapter "$client_interface" "$takeover"
|
||||
prepare_output_dir
|
||||
ssid="$(wm_portal_ssid "$platform")"
|
||||
@@ -143,6 +144,9 @@ case "$command_name" in
|
||||
export PORTAL_STATION_ENV_HOST="$(cd "$(dirname "$station_env")" && pwd)/$(basename "$station_env")"
|
||||
compose_files+=(-f "$root/tests/portal-contract/compose.station.yaml")
|
||||
fi
|
||||
# The contract source is copied into the image; rebuild with Docker cache so
|
||||
# this invocation always tests the checked-out files, not a stale image.
|
||||
docker compose "${compose_files[@]}" build portal-contract
|
||||
docker compose "${compose_files[@]}" run --rm portal-contract
|
||||
printf 'Portal contract passed. Artifacts: %s\n' "$output_dir"
|
||||
if [[ "$keep" == "yes" ]]; then
|
||||
|
||||
@@ -10,6 +10,7 @@ stub_bin="$tmp/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
export CALL_LOG="$tmp/calls.log"
|
||||
export WM_HARDWARE_LOCK_FILE="$tmp/hardware.lock"
|
||||
export XDG_STATE_HOME="$tmp/state"
|
||||
|
||||
printf '%s\n' '#!/usr/bin/env bash' \
|
||||
'if [[ "$1" == "link" && "$2" == "show" ]]; then exit 0; fi' \
|
||||
@@ -18,16 +19,22 @@ printf '%s\n' '#!/usr/bin/env bash' \
|
||||
printf '%s\n' '#!/usr/bin/env bash' \
|
||||
'printf "%s\\n" "$*" >>"$CALL_LOG"' \
|
||||
'if [[ "${NMCLI_FAIL_UP:-}" == "yes" && "$1" == "connection" && "$2" == "up" ]]; then exit 7; fi' \
|
||||
'if [[ "$1" == "-t" && "$2" == "-f" && "$3" == "SSID" ]]; then echo "WM Contract ESP8266"; exit 0; fi' \
|
||||
'if [[ "$1" == "-g" && "$2" == "connection.uuid" ]]; then echo "stub-uuid"; exit 0; fi' \
|
||||
'if [[ "$1" == "-g" ]]; then echo "--"; fi' >"$stub_bin/nmcli"
|
||||
printf '%s\n' '#!/usr/bin/env bash' 'exit 0' >"$stub_bin/pio"
|
||||
printf '%s\n' '#!/usr/bin/env bash' \
|
||||
'printf "docker %s\n" "$*" >>"$CALL_LOG"' \
|
||||
'if [[ "$1" == "compose" && "$2" == "version" ]]; then echo "Docker Compose"; exit 0; fi' \
|
||||
'exit 0' >"$stub_bin/docker"
|
||||
chmod 755 "$stub_bin"/*
|
||||
export PATH="$stub_bin:$PATH"
|
||||
|
||||
"$root/tools/portal-hardware" doctor --client-interface wlan-client >/dev/null
|
||||
! grep -Eq 'connection (add|modify|delete)|device disconnect' "$CALL_LOG"
|
||||
if grep -Eq 'connection (add|modify|delete)|device disconnect' "$CALL_LOG"; then
|
||||
echo 'doctor unexpectedly changed a NetworkManager connection' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if "$root/tools/portal-hardware" doctor --client-interface wlan-main >/dev/null 2>&1; then
|
||||
echo 'default-route adapter guard did not reject the request' >&2
|
||||
@@ -48,6 +55,32 @@ if wm_create_portal_connection wlan-client 'fixture portal' placeholder; then
|
||||
fi
|
||||
unset NMCLI_FAIL_UP
|
||||
grep -Eq 'connection delete wifimanager-portal-' "$CALL_LOG"
|
||||
! grep -Eq 'connection (add|modify|delete)|device disconnect' "$CALL_LOG"
|
||||
|
||||
# A retained session must be removed explicitly, never silently overwritten.
|
||||
wm_write_state wlan-client esp8266 stale-uuid stale-name
|
||||
wm_load_state
|
||||
[[ "$WM_PORTAL_INTERFACE" == "wlan-client" && "$WM_PORTAL_PLATFORM" == "esp8266" ]]
|
||||
[[ "$WM_PORTAL_CONNECTION_UUID" == "stale-uuid" && "$WM_PORTAL_CONNECTION_NAME" == "stale-name" ]]
|
||||
mutations_before="$(grep -Ec '^(device disconnect|connection (add|modify|delete|down))' "$CALL_LOG" || true)"
|
||||
if "$root/tools/portal-hardware" up --platform esp8266 --port /dev/null \
|
||||
--client-interface wlan-client --take-over-client-adapter >/dev/null 2>&1; then
|
||||
echo 'stale portal session was silently overwritten' >&2
|
||||
exit 1
|
||||
fi
|
||||
mutations_after="$(grep -Ec '^(device disconnect|connection (add|modify|delete|down))' "$CALL_LOG" || true)"
|
||||
[[ "$mutations_before" == "$mutations_after" ]] || {
|
||||
echo 'stale portal session mutated the selected adapter' >&2
|
||||
exit 1
|
||||
}
|
||||
wm_clear_state
|
||||
|
||||
# The runner must build the copied contract source before it starts the container.
|
||||
"$root/tools/portal-hardware" run --platform esp8266 --port /dev/null --client-interface wlan-client --browser skip >/dev/null
|
||||
build_line="$(grep -n " build portal-contract$" "$CALL_LOG" | tail -1 | cut -d: -f1)"
|
||||
run_line="$(grep -n " run --rm portal-contract$" "$CALL_LOG" | tail -1 | cut -d: -f1)"
|
||||
[[ -n "$build_line" && -n "$run_line" && "$build_line" -lt "$run_line" ]] || {
|
||||
echo "portal contract was not rebuilt before execution" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo 'portal-hardware CLI safety checks passed'
|
||||
|
||||
Reference in New Issue
Block a user