#!/usr/bin/env bash # The VPN toggle, end to end minus NetworkManager. # # The property under test is the one that motivated the feature: a person # turned on a tunnel whose server was unreachable and had no way back short of # nmcli in a terminal. So beyond parsing and wiring, this pins the safety # behavior — a failed activation is rolled back down and reported, never left # half-up as a black-hole default route. # # nmcli is a stateful stub on PATH: fixtures decide what profiles exist, an # `active` file tracks what is up, and every invocation is recorded. The real # helper and the real service run against it, so the parse, the recency pick, # and the rollback are all the shipped code paths. set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" harness="$repo_dir/config/dot/quickshell/vpn-harness.qml" service="$repo_dir/config/dot/quickshell/services/Vpn.qml" helper="$repo_dir/config/dot/quickshell/scripts/panama-vpn" panel="$repo_dir/config/dot/quickshell/modules/quicksettings/QuickSettingsPanel.qml" vpn_list="$repo_dir/config/dot/quickshell/modules/quicksettings/VpnList.qml" cluster="$repo_dir/config/dot/quickshell/modules/bar/StatusCluster.qml" fail() { printf 'vpn contract: %s\n' "$1" >&2 exit 1 } # ── Wiring that must not silently disappear ────────────────────────────────── rg -Fq 'visible: Vpn.available' "$panel" \ || fail 'the VPN tile is not gated on a profile existing' rg -Fq 'onToggled: Vpn.toggle()' "$panel" \ || fail 'the VPN tile does not drive Vpn.toggle()' rg -Fq 'root.expand("vpn")' "$panel" \ || fail 'the VPN tile cannot open its detail list' rg -Fq 'VpnList {' "$panel" \ || fail 'the VPN detail list is not mounted in the panel' rg -Fq 'visible: Vpn.anyActive' "$cluster" \ || fail 'the bar glyph is not gated on an active tunnel' rg -Fq 'onClicked: Vpn.setActive(modelData.uuid, !modelData.active)' "$vpn_list" \ || fail 'a profile row does not flip that profile' rg -Fq 'visible: Vpn.lastError !== ""' "$vpn_list" \ || fail 'activation failures have nowhere to surface' # The bound and the rollback are what make the toggle safe on a dead server. rg -Fq 'nmcli -w 25 connection up uuid' "$helper" \ || fail 'activation is unbounded -- a dead server hangs the toggle for 90s' rg -q 'nmcli connection down uuid .* 2>&1\|\|nmcli connection down uuid' "$helper" \ || rg -Fq 'nmcli connection down uuid "$uuid" >/dev/null 2>&1 || true' "$helper" \ || fail 'a failed activation is not rolled back down' rg -Fq '"nmcli", "monitor"' "$service" \ || fail 'outside changes to NetworkManager state are never noticed' # ── The stub NetworkManager ────────────────────────────────────────────────── stub_dir="$(mktemp -d)" state_dir="$(mktemp -d)" config_home="$(mktemp -d)" : >"$state_dir/active" : >"$state_dir/log" cat >"$stub_dir/nmcli" <>"\$state/log" case "\$*" in "monitor") exec sleep 45 ;; "-t -f NAME,UUID,TYPE,TIMESTAMP connection show") printf 'Home:uuid-wg-home:wireguard:200\n' printf 'Office\\\\: Berlin:uuid-vpn-office:vpn:100\n' printf "Gib's iPhone:uuid-wifi:802-11-wireless:300\n" ;; "-t -f UUID connection show --active") cat "\$state/active" ;; "-w 25 connection up uuid "*) uuid="\${!#}" if [[ -e "\$state/fail-up" ]]; then echo "Error: Connection activation failed: the server did not respond." >&2 exit 4 fi echo "\$uuid" >>"\$state/active" ;; "connection down uuid "*) uuid="\${!#}" grep -v "^\$uuid\$" "\$state/active" >"\$state/active.next" || true mv "\$state/active.next" "\$state/active" ;; *) echo "stub nmcli: unexpected: \$*" >&2 exit 9 ;; esac STUB chmod +x "$stub_dir/nmcli" stop_harness() { [[ -n "${harness_pid:-}" ]] && kill "$harness_pid" >/dev/null 2>&1 || true rm -rf "$stub_dir" "$state_dir" "$config_home" } trap stop_harness EXIT run() { XDG_CONFIG_HOME="$config_home" PATH="$stub_dir:$PATH" qs -p "$harness" "$@"; } status() { run ipc call vpn-test status; } PATH="$stub_dir:$PATH" XDG_CONFIG_HOME="$config_home" qs -p "$harness" --daemonize >/dev/null for _ in $(seq 1 40); do run ipc show 2>/dev/null | rg -q '^target vpn-test$' && break sleep 0.1 done run ipc show 2>/dev/null | rg -q '^target vpn-test$' || fail 'test IPC target did not start' harness_pid="$(run list | awk '/Process ID:/ { print $3; exit }')" settle() { local want="$1" tries="${2:-50}" for _ in $(seq 1 "$tries"); do if jq -e "$want" <<<"$(status)" >/dev/null 2>&1; then return 0 fi sleep 0.2 done return 1 } # ── Only VPN kinds are listed, names unescape, recency is read ─────────────── settle '.scanned and (.busy | not)' || fail 'the service never finished its first scan' listing="$(status)" jq -e '.connections | length == 2' <<<"$listing" >/dev/null \ || fail "the wifi profile leaked into the VPN list: $listing" jq -e '.connections | map(.name) | sort == ["Home", "Office: Berlin"]' <<<"$listing" >/dev/null \ || fail "names did not parse (escaped colon?): $listing" jq -e '.available and (.anyActive | not)' <<<"$listing" >/dev/null \ || fail "profiles exist but the tile would not show: $listing" # ── Toggle up picks the most recently used profile ─────────────────────────── run ipc call vpn-test toggle >/dev/null settle '.anyActive and (.busy | not)' || fail 'toggling up never activated anything' jq -e '.activeSummary == "Home"' <<<"$(status)" >/dev/null \ || fail "toggle did not pick the most recently used profile: $(status)" rg -Fq -- '-w 25 connection up uuid uuid-wg-home' "$state_dir/log" \ || fail 'the most recently used uuid was not the one activated' # ── Toggle down takes the active tunnel down ───────────────────────────────── run ipc call vpn-test toggle >/dev/null settle '(.anyActive | not) and (.busy | not)' || fail 'toggling down left the tunnel up' rg -Fq 'connection down uuid uuid-wg-home' "$state_dir/log" \ || fail 'deactivation never reached nmcli' # ── A dead server: bounded, rolled back, and reported ──────────────────────── touch "$state_dir/fail-up" run ipc call vpn-test setActive uuid-vpn-office true >/dev/null settle '(.busy | not) and .lastError != ""' || fail 'a failed activation reported nothing' settle '.anyActive | not' 5 || fail 'a failed activation stayed half-up' rg -Fq 'connection down uuid uuid-vpn-office' "$state_dir/log" \ || fail 'a failed activation was not rolled back down' exit 0