#!/usr/bin/env bash # test/integration.sh — integration test sections # Tests run against the live wgctl binary. # Sourced by test.command.sh — do not execute directly. WGCTL_BINARY="$(command -v wgctl)" # ============================================ # Helpers # ============================================ function cmd::test::_strip_ansi() { sed 's/\x1b\[[0-9;]*m//g' } function cmd::test::run_cmd() { local desc="$1" expected="${2:-}" shift 2 local tmp exit_code tmp=$(mktemp) set +e timeout 30 "$WGCTL_BINARY" "$@" > "$tmp" 2>&1 exit_code=$? set -e # Reset terminal color in case command output left ANSI state dirty printf "\033[0m" >&2 if [[ $exit_code -eq 124 ]]; then test::warn "${desc} (timed out after 30s)" rm -f "$tmp" return 1 fi local clean clean=$(cmd::test::_strip_ansi < "$tmp") if [[ $exit_code -ne 0 ]]; then local msg="${desc}" [[ -n "$expected" ]] && msg="${desc} (expected '${expected}', command failed)" test::fail "$msg" if [[ "${WGCTL_TEST_VERBOSE:-false}" == "true" ]]; then printf " Output: %s\n" "$(echo "$clean" | head -3 | tr '\n' ' ')" fi rm -f "$tmp" return 1 fi if [[ -n "$expected" ]] && ! echo "$clean" | grep -qF "$expected"; then local actual actual=$(echo "$clean" | head -3 | tr '\n' ' ' | sed 's/ */ /g' | cut -c1-100) test::fail "${desc} (expected '${expected}', got: '${actual}')" rm -f "$tmp" return 1 fi test::pass "$desc" rm -f "$tmp" } function cmd::test::run_cmd_any() { local desc="$1" expected="${2:-}" shift 2 local tmp tmp=$(mktemp) set +e timeout 30 "$WGCTL_BINARY" "$@" > "$tmp" 2>&1 set -e printf "\033[0m" >&2 local clean clean=$(cmd::test::_strip_ansi < "$tmp") if [[ -n "$expected" ]] && ! echo "$clean" | grep -qF "$expected"; then local actual actual=$(echo "$clean" | head -3 | tr '\n' ' ' | sed 's/ */ /g' | cut -c1-100) test::fail "${desc} (expected '${expected}', got: '${actual}')" rm -f "$tmp" return 1 fi test::pass "$desc" rm -f "$tmp" } function cmd::test::run_cmd_fails() { local desc="$1" shift local tmp exit_code tmp=$(mktemp) set +e timeout 10 "$WGCTL_BINARY" "$@" > "$tmp" 2>&1 exit_code=$? set -e printf "\033[0m" >&2 rm -f "$tmp" if [[ $exit_code -eq 124 ]]; then test::warn "${desc} (timed out)" return 1 fi if [[ $exit_code -eq 0 ]]; then test::fail "${desc} (expected failure but succeeded)" return 1 fi test::pass "$desc" } # ============================================ # Sections # ============================================ function cmd::test::run_all_integration_sections() { cmd::test::section_list cmd::test::section_inspect cmd::test::section_config cmd::test::section_rules cmd::test::section_groups cmd::test::section_audit cmd::test::section_logs cmd::test::section_fw cmd::test::section_net cmd::test::section_subnet cmd::test::section_identity cmd::test::section_activity cmd::test::section_policy cmd::test::section_hosts cmd::test::section_peer_cmd cmd::test::section_group_purge cmd::test::section_logs_clean } function cmd::test::section_list() { test::section "List" cmd::test::run_cmd "list" "rule:" list cmd::test::run_cmd "list --online" "" list --online cmd::test::run_cmd "list --offline" "" list --offline cmd::test::run_cmd "list --blocked" "" list --blocked cmd::test::run_cmd "list --type phone" "phone" list --type phone cmd::test::run_cmd "list --detailed" "rule:" list --detailed cmd::test::run_cmd "list --name phone-nuno" "phone-nuno" list --name phone-nuno cmd::test::run_cmd "list --json" '"ok":true' list --json cmd::test::run_cmd "list --json has peers" '"peers":' list --json cmd::test::run_cmd "list --json has meta" '"meta":' list --json cmd::test::run_cmd "list --json peer name" '"name":' list --json cmd::test::run_cmd "list --json peer ip" '"ip":' list --json cmd::test::run_cmd "list --json peer status" '"status":' list --json } function cmd::test::section_inspect() { test::section "Inspect" cmd::test::run_cmd "inspect --name phone-nuno" "IP:" inspect --name phone-nuno cmd::test::run_cmd "inspect --name nuno --type phone" "IP:" inspect --name nuno --type phone cmd::test::run_cmd "inspect --name phone-nuno --config" "PrivateKey" inspect --name phone-nuno --config cmd::test::run_cmd_fails "inspect nonexistent" inspect --name nonexistent-peer cmd::test::run_cmd "inspect --json" '"ok":true' inspect --name phone-nuno --json cmd::test::run_cmd "inspect --json rule" '"rule":' inspect --name phone-nuno --json cmd::test::run_cmd "inspect --json identity" '"identity":' inspect --name phone-nuno --json cmd::test::run_cmd "inspect --json groups" '"groups":' inspect --name phone-nuno --json } function cmd::test::section_config() { test::section "Config & QR" cmd::test::run_cmd "config --name phone-nuno" "PrivateKey" config --name phone-nuno cmd::test::run_cmd "config --name nuno --type phone" "PrivateKey" config --name nuno --type phone cmd::test::run_cmd "qr --name phone-nuno" "" qr --name phone-nuno } function cmd::test::section_rules() { test::section "Rules" cmd::test::run_cmd "rule list" "user" rule list cmd::test::run_cmd "rule show --name guest" "Description" rule show --name guest cmd::test::run_cmd "rule show --name user" "Description" rule show --name user cmd::test::run_cmd "rule show --name admin" "Description" rule show --name admin cmd::test::run_cmd "rule list --json" '"rules":' rule list --json cmd::test::run_cmd "rule list --json" '"rules":' rule list --json cmd::test::run_cmd "rule --json is_base" '"is_base":' rule list --json cmd::test::run_cmd "rule --json extends" '"extends":' rule list --json cmd::test::run_cmd "rule --json allows" '"allows":' rule list --json cmd::test::run_cmd_fails "rule show nonexistent" rule show --name nonexistent } function cmd::test::section_groups() { test::section "Groups" cmd::test::run_cmd "group list" "Groups" group list cmd::test::run_cmd "group show --name family" "Peers:" group show --name family cmd::test::run_cmd "group list --json" '"groups":' group list --json cmd::test::run_cmd "group list --json" '"groups":' group list --json cmd::test::run_cmd "group --json peer_count" '"peer_count":' group list --json cmd::test::run_cmd_fails "group show nonexistent" group show --name nonexistent } function cmd::test::section_audit() { test::section "Audit" cmd::test::run_cmd_any "audit" "passed" audit cmd::test::run_cmd_any "audit --peer phone-nuno" "passed" audit --peer phone-nuno cmd::test::run_cmd_any "audit --type phone" "passed" audit --type phone } function cmd::test::section_logs() { test::section "Logs" cmd::test::run_cmd "logs" "Activity" logs cmd::test::run_cmd "logs --name phone-nuno" "Activity" logs --name phone-nuno cmd::test::run_cmd "logs --fw" "Firewall Drops" logs --fw cmd::test::run_cmd "logs --wg" "WireGuard Events" logs --wg cmd::test::run_cmd "logs --since 2099-01-01" "No logs" logs --since "2099-01-01" cmd::test::run_cmd "logs --wg --since 2099-01-01" "No logs" logs --wg --since "2099-01-01" cmd::test::run_cmd "logs --fw --since 2099-01-01" "No logs" logs --fw --since "2099-01-01" cmd::test::run_cmd "logs --wg --event attempt" "" logs --wg --event attempt cmd::test::run_cmd "logs --detailed" "" logs --detailed cmd::test::run_cmd "logs --resolved" "" logs --resolved cmd::test::run_cmd "logs --ascending" "" logs --ascending cmd::test::run_cmd "logs --descending" "" logs --descending cmd::test::run_cmd "logs --wg --ascending" "" logs --wg --ascending } function cmd::test::section_fw() { test::section "Firewall" cmd::test::run_cmd "fw list" "FORWARD" fw list cmd::test::run_cmd "fw list --peer phone-nuno" "" fw list --peer phone-nuno cmd::test::run_cmd "fw list --no-nflog" "" fw list --no-nflog cmd::test::run_cmd "fw list --no-accept" "" fw list --no-accept cmd::test::run_cmd "fw list --no-drop" "" fw list --no-drop cmd::test::run_cmd "fw nat" "PREROUTING" fw nat cmd::test::run_cmd "fw count" "TOTAL" fw count } function cmd::test::section_net() { test::section "Net" "$WGCTL_BINARY" net rm --name test-svc --force > /dev/null 2>&1 || true cmd::test::run_cmd "net add service" "added" net add --name test-svc --ip 10.0.0.99 --desc "Test service" cmd::test::run_cmd "net add port" "Added" net add --name test-svc:web --port 9999:tcp cmd::test::run_cmd "net list" "test-svc" net list cmd::test::run_cmd "net list --detailed" "web" net list --detailed cmd::test::run_cmd "net show" "9999" net show --name test-svc cmd::test::run_cmd "net rm port" "Removed" net rm --name test-svc:web --force cmd::test::run_cmd "net add port again" "Added" net add --name test-svc:web --port 9999:tcp cmd::test::run_cmd "net rm all ports" "Removed" net rm --name test-svc:ports --force cmd::test::run_cmd "net rm service" "Removed" net rm --name test-svc --force cmd::test::run_cmd "net list --json" '"services":' net list --json cmd::test::run_cmd "net --json has tags" '"tags":' net list --json cmd::test::run_cmd "net --json port_count" '"port_count":' net list --json cmd::test::run_cmd_fails "net show nonexistent" net show --name nonexistent-svc cmd::test::run_cmd_fails "net add port no service" net add --name nonexistent:web --port 80:tcp } function cmd::test::section_subnet() { test::section "Subnet" "$WGCTL_BINARY" subnet rm --name test-subnet-2 > /dev/null 2>&1 || true "$WGCTL_BINARY" subnet rm --name test-subnet > /dev/null 2>&1 || true cmd::test::run_cmd "subnet list" "desktop" subnet list cmd::test::run_cmd "subnet show desktop" "tunnel:" subnet show --name desktop cmd::test::run_cmd "subnet show guests group" "guests" subnet show --name guests cmd::test::run_cmd_fails "subnet show nonexistent" subnet show --name nonexistent cmd::test::run_cmd "subnet add" "added" subnet add --name test-subnet --subnet 10.1.250.0/24 --type iot --desc "Test" cmd::test::run_cmd "subnet list shows new" "test-subnet" subnet list cmd::test::run_cmd_fails "subnet rename in-use (desktop)" subnet rename --name desktop --new-name workstation cmd::test::run_cmd "subnet rename unused" "renamed" subnet rename --name test-subnet --new-name test-subnet-2 cmd::test::run_cmd "subnet rm" "removed" subnet rm --name test-subnet-2 cmd::test::run_cmd "subnet list --json" '"subnets":' subnet list --json cmd::test::run_cmd "subnet --json has cidr" '"cidr":' subnet list --json cmd::test::run_cmd "subnet --json is_group" '"is_group":' subnet list --json cmd::test::run_cmd_fails "subnet rm nonexistent" subnet rm --name nonexistent-subnet } function cmd::test::section_identity() { test::section "Identity" cmd::test::run_cmd "identity list" "" identity list cmd::test::run_cmd "identity migrate --dry-run" "Dry run" identity migrate --dry-run cmd::test::run_cmd "identity show nuno" "nuno" identity show --name nuno cmd::test::run_cmd "identity list --json" '"identities":' identity list --json cmd::test::run_cmd "identity --json types array" '"types":[' identity list --json cmd::test::run_cmd "identity --json rules array" '"rules":[' identity list --json cmd::test::run_cmd_fails "identity show nonexistent" identity show --name nonexistent } function cmd::test::section_activity() { test::section "Activity" cmd::test::run_cmd "activity" "Activity" activity cmd::test::run_cmd "activity --json" '"peers":' activity --json cmd::test::run_cmd "activity --json services" '"services":' activity --json cmd::test::run_cmd "activity --json rx" '"rx":' activity --json } function cmd::test::section_policy() { test::section "Policy" cmd::test::run_cmd "policy list --json" '"policies":' policy list --json cmd::test::run_cmd "policy --json has tunnel_mode" '"tunnel_mode":' policy list --json cmd::test::run_cmd "policy --json strict_rule bool" '"strict_rule":false' policy list --json } function cmd::test::section_hosts() { test::section "Hosts" # Cleanup "$WGCTL_BINARY" hosts rm --ip 192.0.2.1 --force > /dev/null 2>&1 || true "$WGCTL_BINARY" hosts rm --port 9999 --force > /dev/null 2>&1 || true cmd::test::run_cmd "hosts list" "Hosts" hosts list cmd::test::run_cmd "hosts add --ip" "Added" hosts add --ip 192.0.2.1 --name test-host --desc "Test" --tags test,unit cmd::test::run_cmd "hosts list shows new" "test-host" hosts list cmd::test::run_cmd "hosts show --ip" "Name" hosts show --ip 192.0.2.1 cmd::test::run_cmd "hosts add --port" "Added" hosts add --port 9999 --name test-port cmd::test::run_cmd "hosts list shows port" "test-port" hosts list cmd::test::run_cmd "hosts rm --ip" "Removed" hosts rm --ip 192.0.2.1 --force cmd::test::run_cmd "hosts rm --port" "Removed" hosts rm --port 9999 --force cmd::test::run_cmd "hosts list --json" '"hosts":' hosts list --json cmd::test::run_cmd "hosts --json has type" '"type":' hosts list --json cmd::test::run_cmd "hosts --json has tags" '"tags":' hosts list --json cmd::test::run_cmd_fails "hosts show nonexistent" hosts show --ip 192.0.2.99 cmd::test::run_cmd_fails "hosts add missing --name" hosts add --ip 192.0.2.1 } function cmd::test::section_peer_cmd() { test::section "Peer command" "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true "$WGCTL_BINARY" add --name testunit --type phone > /dev/null 2>&1 # update-dns cmd::test::run_cmd "peer update-dns --name" "Updated DNS" peer update-dns --name phone-testunit cmd::test::run_cmd "peer update-dns applies" "10.0.0.103" config --name phone-testunit cmd::test::run_cmd "peer update-dns --all" "peer(s)" peer update-dns --all # update-tunnel cmd::test::run_cmd "peer update-tunnel split" "split" peer update-tunnel --name phone-testunit --mode split cmd::test::run_cmd "peer update-tunnel full" "Updated" peer update-tunnel --name phone-testunit --mode full cmd::test::run_cmd_fails "peer update-tunnel bad mode" peer update-tunnel --name phone-testunit --mode invalid cmd::test::run_cmd_fails "peer update-tunnel missing --mode" peer update-tunnel --name phone-testunit cmd::test::run_cmd_fails "peer update-tunnel missing --name" peer update-tunnel --mode split # Restore split tunnel "$WGCTL_BINARY" peer update-tunnel --name phone-testunit --mode split > /dev/null 2>&1 || true "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true } function cmd::test::section_group_purge() { test::section "Group: purge-stale" # dry-run should not modify anything cmd::test::run_cmd "purge-stale --all --dry-run" \ "[dry-run]" \ group purge-stale --all --dry-run --force # single group dry-run cmd::test::run_cmd "purge-stale --name family --dry-run" \ "[dry-run]" \ group purge-stale --name family --dry-run --force } function cmd::test::section_logs_clean() { test::section "Logs: clean" cmd::test::run_cmd "logs clean --force" \ "keepalive" \ logs clean --force }