wgctl/commands/test.command.sh

340 lines
No EOL
11 KiB
Bash

#!/usr/bin/env bash
WGCTL_BINARY="/usr/local/bin/wgctl"
# ============================================
# Lifecycle
# ============================================
function cmd::test::on_load() {
flag::register --destructive
flag::register --section
flag::register --fn
flag::register --function
}
function cmd::test::help() {
cat <<EOF
Usage: wgctl test [options]
Run the wgctl test suite.
Options:
--destructive Include tests that modify state (add/remove/block)
--section <name> Run only a specific section (list, rules, groups, audit, logs, fw)
Examples:
wgctl test
wgctl test --section rules
wgctl test --destructive
EOF
}
# ============================================
# Test helpers
# ============================================
function cmd::test::run_cmd() {
local desc="$1"
local expected="${2:-}"
shift 2
local tmp exit_code
tmp=$(mktemp)
set +e # disable exit on error (return 1)
timeout 30 "$WGCTL_BINARY" "$@" > "$tmp" 2>&1 &
local pid=$!
wait $pid
exit_code=$?
set -e # re-enable exit on error
if [[ $exit_code -eq 124 ]]; then
test::warn "${desc} (timed out after 30s)"
rm -f "$tmp"
return 1
fi
if [[ $exit_code -ne 0 ]]; then
test::fail "${desc}"
if [[ "${WGCTL_TEST_VERBOSE:-false}" == "true" ]]; then
printf " Output: %s\n" "$(cat "$tmp")"
fi
rm -f "$tmp"
return 1
fi
if [[ -n "$expected" ]] && ! grep -qF "$expected" "$tmp"; then
test::fail "${desc} (expected '${expected}' in output)"
rm -f "$tmp"
return 1
fi
test::pass "$desc"
rm -f "$tmp"
}
function cmd::test::run_cmd_fails() {
local desc="$1"
shift
set +e # disable exit on error (return 1)
local tmp exit_code
tmp=$(mktemp)
timeout 10 setsid "$WGCTL_BINARY" "$@" > "$tmp" 2>&1
exit_code=$?
set -e # re-enable exit on error
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"
}
function cmd::test::run_function() {
local fn="$1"
local namespace
namespace=$(echo "$fn" | cut -d':' -f3)
load_command "$namespace" 2>/dev/null || true
test::reset
log::section "Function Test: ${fn}"
case "$fn" in
cmd::block::run) cmd::test::fn_block ;;
cmd::unblock::run) cmd::test::fn_unblock ;;
cmd::remove::run) cmd::test::fn_remove ;;
cmd::rule::assign) cmd::test::fn_rule_assign ;;
*)
log::error "No function test defined for: ${fn}"
return 1
;;
esac
test::summary
}
# ============================================
# Test sections
# ============================================
function cmd::test::section_list() {
test::section "List"
cmd::test::run_cmd "list" "WireGuard Clients" list
cmd::test::run_cmd "list --online" "STATUS" list --online
cmd::test::run_cmd "list --offline" "STATUS" list --offline
cmd::test::run_cmd "list --blocked" "STATUS" list --blocked
cmd::test::run_cmd "list --type phone" "phone" list --type phone
cmd::test::run_cmd "list --type guest" "guest" list --type guest
# TODO: Fix detailed, hangs
# cmd::test::run_cmd "list --detailed" "Client:" list --detailed
cmd::test::run_cmd "list --name phone-nuno" "phone-nuno" list --name phone-nuno
}
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 peer" inspect --name nonexistent-peer
}
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" "guest" rule list
cmd::test::run_cmd "rule show --name guest" "Description" rule show --name guest
cmd::test::run_cmd "rule show --name guest --peers" "Peers:" rule show --name guest --peers
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_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_fails "group show nonexistent" group show --name nonexistent
}
function cmd::test::section_audit() {
test::section "Audit"
cmd::test::run_cmd "audit" "passed" audit
cmd::test::run_cmd "audit --peer phone-nuno" "passed" audit --peer phone-nuno
cmd::test::run_cmd "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 --type guest" "Activity" logs --type guest
cmd::test::run_cmd "logs --fw" "Activity" logs --fw
cmd::test::run_cmd "logs --wg" "Activity" logs --wg
}
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_destructive() {
test::section "Destructive (modifying state)"
# Cleanup from any previous failed run
"$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
"$WGCTL_BINARY" group remove --name testgroup --force > /dev/null 2>&1 || true
# Add test peer
cmd::test::run_cmd "add phone peer" "added successfully" \
add --name testunit --type phone
# Block/unblock
cmd::test::run_cmd "block peer" "blocked" \
block --name phone-testunit
cmd::test::run_cmd "list shows blocked" "blocked" \
list --blocked
cmd::test::run_cmd "unblock peer" "unblocked" \
unblock --name phone-testunit
# Rule assign/unassign
cmd::test::run_cmd "rule assign" "Assigned" \
rule assign --name admin --peer phone-testunit
cmd::test::run_cmd "rule unassign" "Unassigned" \
rule unassign --peer phone-testunit
# Group operations
cmd::test::run_cmd "group add" "created" \
group add --name testgroup --desc "Test group"
cmd::test::run_cmd "group peer add" "Added" \
group peer add --name testgroup --peer phone-testunit
cmd::test::run_cmd "group block" "have been blocked" \
group block --name testgroup
cmd::test::run_cmd "group unblock" "have been unblocked" \
group unblock --name testgroup
cmd::test::run_cmd "group remove" "removed" \
group remove --name testgroup --force
# Remove test peer
cmd::test::run_cmd "remove phone peer" "removed" \
remove --name phone-testunit --force
}
# ============================================
# Function Blocks
# ============================================
function cmd::test::fn_block() {
test::section "cmd::block::run"
# Setup
"$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
"$WGCTL_BINARY" add --name testunit --type phone > /dev/null 2>&1
# Tests
cmd::test::run_cmd "block peer" "blocked" block --name phone-testunit
cmd::test::run_cmd "block already blocked" "already" block --name phone-testunit
"$WGCTL_BINARY" unblock --name phone-testunit > /dev/null 2>&1 || true
cmd::test::run_cmd "block with --type" "blocked" block --name testunit --type phone
cmd::test::run_cmd_fails "block nonexistent" block --name truly-nonexistent-xyz
# Cleanup
"$WGCTL_BINARY" unblock --name phone-testunit > /dev/null 2>&1 || true
"$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
}
# ============================================
# Run
# ============================================
function cmd::test::run() {
local destructive=false section=""
local fn=""
while [[ $# -gt 0 ]]; do
case "$1" in
--destructive) destructive=true; shift ;;
--section)
util::require_flag "--section" "${2:-}" || return 1
section="$2"; shift 2
;;
--fn|--function) fn="$2"; shift 2 ;;
--verbose|-v) WGCTL_TEST_VERBOSE=true; shift ;;
--help) cmd::test::help; return ;;
*)
log::error "Unknown flag: $1"
return 1
;;
esac
done
# After flag parsing:
if [[ -n "$fn" ]]; then
cmd::test::run_function "$fn"
return
fi
test::reset
log::section "wgctl Test Suite"
if [[ -n "$section" ]]; then
case "$section" in
list) cmd::test::section_list ;;
inspect) cmd::test::section_inspect ;;
config) cmd::test::section_config ;;
rules) cmd::test::section_rules ;;
groups) cmd::test::section_groups ;;
audit) cmd::test::section_audit ;;
logs) cmd::test::section_logs ;;
fw) cmd::test::section_fw ;;
destructive) cmd::test::section_destructive ;;
*)
log::error "Unknown section: $section"
return 1
;;
esac
else
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
fi
if $destructive; then
cmd::test::section_destructive
fi
test::summary
}