vpn-switch-unit-test.sh
Rama main · réplica de lectura del repositorio original.
#!/bin/sh
#
# === AUTO-GENERATED METADATA (make metadata) ===
# Generated: 2026-11-01 06:41:49
# Terminal functions (single underscore): output shell commands
# Combinator functions (double underscore): output single vpn-switch command
# Batch-combinator functions (triple underscore): output multiple vpn-switch commands
TERMINAL_FUNCTIONS="_comment1 _note1 _error1 _error2 _error3 _fail2 _log1 _log2 _log3 _wireguard_collect0 _openvpn_collect0 _export_fresh1 _dump_header1 _restore_prune2 _filter_write3 _bundle_pack2 _seal_write2 _incoming_remove1 _extract_unpack2 _attest_sign2 _lift1 _database_init0 _environment_init1 _environment_cache1 _lift3 _setenv2 _getenv1 _unsetenv1 _cat1 _batch2 _printenv0 _env_sync0 _version_sync0 _version0 _link2 _remove1 _status0 _help0 _help1 _help2 _complete0 _complete1 _complete2 _complete3 _complete4 _complete5 _complete6 _helpenv0 _helpenv2 _database_enumerate0 _system_inspect0 _logs_inspect0 _phases_inspect0 _summary_inspect0 _prologue_inspect0 _key_import_copy3 _key_collect_files2 _manifest_write2 _openpgp_record2 _openpgp_record3 _openpgp_prerequisites0 _openvpn_patch2 _openvpn_configure1 _openvpn_connect1 _openvpn_disconnect1 _openvpn_patch1 _openvpn_enumerate0 _openvpn_import1 _openvpn_list0 _openvpn_list1 _openvpn_add1 _openvpn_add3 _openvpn_link2 _openvpn_remove1 _openvpn_remove2 _openvpn_validate0 _openvpn_info1 _phases_copy0 _provenance_serial1 _provenance_file5 _provenance_import_copy2 _provenance_collect0 _provenance_list1 _session_list0 _session_show1 _session_remove1 _session_connect1 _session_save0 _session_save1 _session_clean0 _session_refresh0 _session_validate0 _session_enumerate0 _session_describe0 _session_create1 _interface_destroy1 _session_populate3 _logs_validate0 _logs_clean0 _phases_validate0 _database_validate0 _binaries_validate0 _network_validate0 _permissions_validate0 _environment_validate0 _install_validate0 _version_validate0 _sudo_validate0 _wireguard_patch2 _wireguard_configure1 _wireguard_connect1 _wireguard_disconnect1 _wireguard_enumerate0 _wireguard_import1 _wireguard_list0 _wireguard_list1 _wireguard_add1 _wireguard_add3 _wireguard_link2 _wireguard_remove1 _wireguard_remove2 _wireguard_validate0 _wireguard_patch1 _wireguard_info1"
COMBINATOR_FUNCTIONS="__export3 __export2 __export_redacted2 __export_full2 __export_minimized2 __export_pair3 __dump_readable1 __archive_key_pinned0 __restore1 __restore2 __restore_v12 __restore_v13 __restore_v14 __restore_v15 __restore3 __restore_serial_admissible3 __restore_base_exists1 __restore_replay2 __filter2 __filter_default2 __filter3 __filter_redacted2 __filter_full2 __filter_minimized2 __collection_readable1 __archiver_set0 __bundle_archive_absolute1 __bundle_manifest_beside1 __bundle_collection_inside1 __bundle_readable1 __seal_absent1 __attest_absent1 __seal_present1 __seal_matches2 __incoming_clear1 __extractor_set0 __extract_destination_absolute1 __extract_destination_empty1 __import2 __import_bootstrap2 __import3 __import4 __import5 __import6 __attest_file_exists1 __openpgp_record_complete1 __attest_signer_pinned2 __bootstrap1 __environment_cache0 __phases_lift1 __init0 __dump0 __batch1 __import1 __helpenv1 __setintp2 __getintp1 __unsetintp1 __helpintp1 __helpintp2 __key_name_valid1 __base_element_exists1 __key_record_exists2 __manifest_named1 __manifest_entries_hashable1 __manifest_readable1 __manifest_base_exists1 __manifest_tree_matches2 __openpgp_keyid_valid1 __openpgp_import2 __openpgp_collect1 __openvpn_start0 __openvpn_start1 __openvpn_stop1 __openvpn_add2 __openvpn_help0 __provenance_serial0 __provenance_add2 __provenance_add3 __provenance_add4 __provenance_add5 __provenance_add6 __provenance_bundle_readable1 __provenance_receipt5 __provenance_receipt_same3 __provenance_id_valid1 __provenance_import_place2 __provenance_list0 __session_disconnect1 __session_stop1 __session_show0 __session_start0 __session_start1 __session_help0 __session_exists1 __session_conflicts2 __interface_check1 __stop1 __start1 __start0 __wireguard_start0 __wireguard_start1 __wireguard_stop1 __wireguard_add2 __wireguard_help0"
BATCH_COMBINATOR_FUNCTIONS="___collect0 ___export_pair4 ___restore_v16 ___bundle2 ___seal2 ___seal_verify2 ___extract2 ___import_bootstrap5 ___import_bootstrap6 ___import7 ___attest2 ___attest_verify2 ___bootstrap2 ___environment_refresh0 ___init1 ___dumpenv1 ___dump1 ___batch0 ___sync0 ___list0 ___environment_inspect0 ___inspect0 ___key_import3 ___manifest2 ___manifest_attest2 ___manifest_verify3 ___manifest_match2 ___openpgp_add2 ___openpgp_add3 ___openpgp_dump0 ___openpgp_collect0 ___openvpn_create1 ___openvpn_stop0 ___openvpn_inspect0 ___openvpn_clean0 ___openvpn_dump0 ___phases_sync1 ___phases_sync0 ___provenance_add7 ___provenance_import2 ___provenance_dump0 ___session_stop0 ___session_switch1 ___session_switch2 ___session_dump0 ___session_inspect0 ___session_prepare3 ___stop0 ___validate0 ___wireguard_create1 ___wireguard_inspect0 ___wireguard_stop0 ___wireguard_clean0 ___wireguard_clean1 ___wireguard_dump0"
ANCHOR_FUNCTIONS="_comment1 _note1 _error1 _error2 _error3 _fail2 _log1 _log2 _log3 _wireguard_collect0 _openvpn_collect0 _export_fresh1 _dump_header1 _restore_prune2 _filter_write3 _bundle_pack2 _seal_write2 _incoming_remove1 _extract_unpack2 _attest_sign2 _lift1 _database_init0 _environment_init1 _environment_cache1 _lift3 _setenv2 _getenv1 _unsetenv1 _cat1 _batch2 _printenv0 _env_sync0 _version_sync0 _version0 _link2 _remove1 _status0 _help0 _help1 _help2 _complete0 _complete1 _complete2 _complete3 _complete4 _complete5 _complete6 _helpenv0 _helpenv2 _database_enumerate0 _system_inspect0 _logs_inspect0 _phases_inspect0 _summary_inspect0 _prologue_inspect0 _key_import_copy3 _key_collect_files2 _manifest_write2 _openpgp_record2 _openpgp_record3 _openpgp_prerequisites0 _openvpn_patch2 _openvpn_configure1 _openvpn_connect1 _openvpn_disconnect1 _openvpn_patch1 _openvpn_enumerate0 _openvpn_import1 _openvpn_list0 _openvpn_list1 _openvpn_add1 _openvpn_add3 _openvpn_link2 _openvpn_remove1 _openvpn_remove2 _openvpn_validate0 _openvpn_info1 _phases_copy0 _provenance_serial1 _provenance_file5 _provenance_import_copy2 _provenance_collect0 _provenance_list1 _session_list0 _session_show1 _session_remove1 _session_connect1 _session_save0 _session_save1 _session_clean0 _session_refresh0 _session_validate0 _session_enumerate0 _session_describe0 _session_create1 _interface_destroy1 _session_populate3 _logs_validate0 _logs_clean0 _phases_validate0 _database_validate0 _binaries_validate0 _network_validate0 _permissions_validate0 _environment_validate0 _install_validate0 _version_validate0 _sudo_validate0 _wireguard_patch2 _wireguard_configure1 _wireguard_connect1 _wireguard_disconnect1 _wireguard_enumerate0 _wireguard_import1 _wireguard_list0 _wireguard_list1 _wireguard_add1 _wireguard_add3 _wireguard_link2 _wireguard_remove1 _wireguard_remove2 _wireguard_validate0 _wireguard_patch1 _wireguard_info1 __export3 __export2 __export_redacted2 __export_full2 __export_minimized2 __export_pair3 __dump_readable1 __archive_key_pinned0 __restore1 __restore2 __restore_v12 __restore_v13 __restore_v14 __restore_v15 __restore3 __restore_serial_admissible3 __restore_base_exists1 __restore_replay2 __filter2 __filter_default2 __filter3 __filter_redacted2 __filter_full2 __filter_minimized2 __collection_readable1 __archiver_set0 __bundle_archive_absolute1 __bundle_manifest_beside1 __bundle_collection_inside1 __bundle_readable1 __seal_absent1 __attest_absent1 __seal_present1 __seal_matches2 __incoming_clear1 __extractor_set0 __extract_destination_absolute1 __extract_destination_empty1 __import2 __import_bootstrap2 __import3 __import4 __import5 __import6 __attest_file_exists1 __openpgp_record_complete1 __attest_signer_pinned2 __bootstrap1 __environment_cache0 __phases_lift1 __init0 __dump0 __batch1 __import1 __helpenv1 __setintp2 __getintp1 __unsetintp1 __helpintp1 __helpintp2 __key_name_valid1 __base_element_exists1 __key_record_exists2 __manifest_named1 __manifest_entries_hashable1 __manifest_readable1 __manifest_base_exists1 __manifest_tree_matches2 __openpgp_keyid_valid1 __openpgp_import2 __openpgp_collect1 __openvpn_start0 __openvpn_start1 __openvpn_stop1 __openvpn_add2 __openvpn_help0 __provenance_serial0 __provenance_add2 __provenance_add3 __provenance_add4 __provenance_add5 __provenance_add6 __provenance_bundle_readable1 __provenance_receipt5 __provenance_receipt_same3 __provenance_id_valid1 __provenance_import_place2 __provenance_list0 __session_disconnect1 __session_stop1 __session_show0 __session_start0 __session_start1 __session_help0 __session_exists1 __session_conflicts2 __interface_check1 __stop1 __start1 __start0 __wireguard_start0 __wireguard_start1 __wireguard_stop1 __wireguard_add2 __wireguard_help0 ___collect0 ___export_pair4 ___restore_v16 ___bundle2 ___seal2 ___seal_verify2 ___extract2 ___import_bootstrap5 ___import_bootstrap6 ___import7 ___attest2 ___attest_verify2 ___bootstrap2 ___environment_refresh0 ___init1 ___dumpenv1 ___dump1 ___batch0 ___sync0 ___list0 ___environment_inspect0 ___inspect0 ___key_import3 ___manifest2 ___manifest_attest2 ___manifest_verify3 ___manifest_match2 ___openpgp_add2 ___openpgp_add3 ___openpgp_dump0 ___openpgp_collect0 ___openvpn_create1 ___openvpn_stop0 ___openvpn_inspect0 ___openvpn_clean0 ___openvpn_dump0 ___phases_sync1 ___phases_sync0 ___provenance_add7 ___provenance_import2 ___provenance_dump0 ___session_stop0 ___session_switch1 ___session_switch2 ___session_dump0 ___session_inspect0 ___session_prepare3 ___stop0 ___validate0 ___wireguard_create1 ___wireguard_inspect0 ___wireguard_stop0 ___wireguard_clean0 ___wireguard_clean1 ___wireguard_dump0"
# Function-to-module mapping (format: "func:module.sh func:module.sh ...")
# Used by process_arguments() for deterministic module loading
FUNCTION_MODULES="___collect0:archive.sh _wireguard_collect0:archive.sh _openvpn_collect0:archive.sh __export3:archive.sh __export2:archive.sh __export_redacted2:archive.sh __export_full2:archive.sh __export_minimized2:archive.sh __export_pair3:archive.sh _export_fresh1:archive.sh ___export_pair4:archive.sh _dump_header1:archive.sh __dump_readable1:archive.sh __archive_key_pinned0:archive.sh __restore1:archive.sh __restore2:archive.sh __restore_v12:archive.sh __restore_v13:archive.sh __restore_v14:archive.sh __restore_v15:archive.sh ___restore_v16:archive.sh _restore_prune2:archive.sh __restore3:archive.sh __restore_serial_admissible3:archive.sh __restore_base_exists1:archive.sh __restore_replay2:archive.sh __filter2:archive.sh __filter_default2:archive.sh __filter3:archive.sh __filter_redacted2:archive.sh __filter_full2:archive.sh __filter_minimized2:archive.sh _filter_write3:archive.sh ___bundle2:archive.sh __collection_readable1:archive.sh __archiver_set0:archive.sh __bundle_archive_absolute1:archive.sh __bundle_manifest_beside1:archive.sh __bundle_collection_inside1:archive.sh _bundle_pack2:archive.sh ___seal2:archive.sh __bundle_readable1:archive.sh __seal_absent1:archive.sh __attest_absent1:archive.sh _seal_write2:archive.sh ___seal_verify2:archive.sh __seal_present1:archive.sh __seal_matches2:archive.sh __incoming_clear1:archive.sh _incoming_remove1:archive.sh ___extract2:archive.sh __extractor_set0:archive.sh __extract_destination_absolute1:archive.sh __extract_destination_empty1:archive.sh _extract_unpack2:archive.sh __import2:archive.sh __import_bootstrap2:archive.sh ___import_bootstrap5:archive.sh ___import_bootstrap6:archive.sh __import3:archive.sh __import4:archive.sh __import5:archive.sh __import6:archive.sh ___import7:archive.sh ___attest2:attest.sh __attest_file_exists1:attest.sh __openpgp_record_complete1:attest.sh _attest_sign2:attest.sh ___attest_verify2:attest.sh __attest_signer_pinned2:attest.sh __bootstrap1:database.sh ___bootstrap2:database.sh _lift1:database.sh _database_init0:database.sh _environment_init1:database.sh ___environment_refresh0:database.sh __environment_cache0:database.sh _environment_cache1:database.sh _lift3:database.sh __phases_lift1:database.sh ___init1:database.sh __init0:database.sh _setenv2:database.sh _getenv1:database.sh _unsetenv1:database.sh ___dumpenv1:database.sh __dump0:database.sh ___dump1:database.sh _cat1:database.sh ___batch0:database.sh __batch1:database.sh _batch2:database.sh _printenv0:database.sh ___sync0:database.sh _env_sync0:database.sh _version_sync0:database.sh _version0:database.sh __import1:database.sh ___list0:database.sh _link2:database.sh _remove1:database.sh _status0:help.sh _help0:help.sh _help1:help.sh _help2:help.sh _complete0:help.sh _complete1:help.sh _complete2:help.sh _complete3:help.sh _complete4:help.sh _complete5:help.sh _complete6:help.sh _helpenv0:helpenv.sh __helpenv1:helpenv.sh _helpenv2:helpenv.sh __setintp2:helpenv.sh __getintp1:helpenv.sh __unsetintp1:helpenv.sh __helpintp1:helpenv.sh __helpintp2:helpenv.sh _database_enumerate0:inspect.sh ___environment_inspect0:inspect.sh _system_inspect0:inspect.sh _logs_inspect0:inspect.sh _phases_inspect0:inspect.sh _summary_inspect0:inspect.sh _prologue_inspect0:inspect.sh ___inspect0:inspect.sh __key_name_valid1:keys.sh ___key_import3:keys.sh __base_element_exists1:keys.sh _key_import_copy3:keys.sh _key_collect_files2:keys.sh __key_record_exists2:keys.sh ___manifest2:manifest.sh __manifest_named1:manifest.sh __manifest_entries_hashable1:manifest.sh _manifest_write2:manifest.sh ___manifest_attest2:manifest.sh ___manifest_verify3:manifest.sh ___manifest_match2:manifest.sh __manifest_readable1:manifest.sh __manifest_base_exists1:manifest.sh __manifest_tree_matches2:manifest.sh ___openpgp_add2:openpgp.sh ___openpgp_add3:openpgp.sh __openpgp_keyid_valid1:openpgp.sh _openpgp_record2:openpgp.sh _openpgp_record3:openpgp.sh _openpgp_prerequisites0:openpgp.sh ___openpgp_dump0:openpgp.sh __openpgp_import2:openpgp.sh ___openpgp_collect0:openpgp.sh __openpgp_collect1:openpgp.sh __openvpn_start0:openvpn.sh __openvpn_start1:openvpn.sh ___openvpn_create1:openvpn.sh _openvpn_patch2:openvpn.sh _openvpn_configure1:openvpn.sh _openvpn_connect1:openvpn.sh ___openvpn_stop0:openvpn.sh __openvpn_stop1:openvpn.sh _openvpn_disconnect1:openvpn.sh _openvpn_patch1:openvpn.sh ___openvpn_inspect0:openvpn.sh _openvpn_enumerate0:openvpn.sh _openvpn_import1:openvpn.sh _openvpn_list0:openvpn.sh _openvpn_list1:openvpn.sh _openvpn_add1:openvpn.sh __openvpn_add2:openvpn.sh _openvpn_add3:openvpn.sh _openvpn_link2:openvpn.sh _openvpn_remove1:openvpn.sh _openvpn_remove2:openvpn.sh _openvpn_validate0:openvpn.sh ___openvpn_clean0:openvpn.sh _openvpn_info1:openvpn.sh ___openvpn_dump0:openvpn.sh __openvpn_help0:openvpn.sh _phases_copy0:phase.sh ___phases_sync1:phase.sh ___phases_sync0:phase.sh __provenance_serial0:provenance.sh _provenance_serial1:provenance.sh __provenance_add2:provenance.sh __provenance_add3:provenance.sh __provenance_add4:provenance.sh __provenance_add5:provenance.sh __provenance_add6:provenance.sh ___provenance_add7:provenance.sh __provenance_bundle_readable1:provenance.sh __provenance_receipt5:provenance.sh __provenance_receipt_same3:provenance.sh _provenance_file5:provenance.sh ___provenance_import2:provenance.sh __provenance_id_valid1:provenance.sh __provenance_import_place2:provenance.sh _provenance_import_copy2:provenance.sh ___provenance_dump0:provenance.sh _provenance_collect0:provenance.sh __provenance_list0:provenance.sh _provenance_list1:provenance.sh __session_disconnect1:session.sh ___session_stop0:session.sh __session_stop1:session.sh _session_list0:session.sh __session_show0:session.sh _session_show1:session.sh _session_remove1:session.sh __session_start0:session.sh __session_start1:session.sh ___session_switch1:session.sh ___session_switch2:session.sh _session_connect1:session.sh _session_save0:session.sh _session_save1:session.sh _session_clean0:session.sh _session_refresh0:session.sh _session_validate0:session.sh ___session_dump0:session.sh __session_help0:session.sh ___session_inspect0:session.sh _session_enumerate0:session.sh _session_describe0:session.sh _session_create1:session.sh __session_exists1:session.sh __session_conflicts2:session.sh _interface_destroy1:session.sh __interface_check1:session.sh ___session_prepare3:session.sh _session_populate3:session.sh ___stop0:session.sh __stop1:session.sh __start1:session.sh __start0:session.sh _logs_validate0:validate.sh _logs_clean0:validate.sh _phases_validate0:validate.sh _database_validate0:validate.sh _binaries_validate0:validate.sh _network_validate0:validate.sh _permissions_validate0:validate.sh _environment_validate0:validate.sh _install_validate0:validate.sh _version_validate0:validate.sh _sudo_validate0:validate.sh ___validate0:validate.sh __wireguard_start0:wireguard.sh __wireguard_start1:wireguard.sh ___wireguard_create1:wireguard.sh _wireguard_patch2:wireguard.sh _wireguard_configure1:wireguard.sh _wireguard_connect1:wireguard.sh _wireguard_disconnect1:wireguard.sh ___wireguard_inspect0:wireguard.sh _wireguard_enumerate0:wireguard.sh ___wireguard_stop0:wireguard.sh __wireguard_stop1:wireguard.sh _wireguard_import1:wireguard.sh _wireguard_list0:wireguard.sh _wireguard_list1:wireguard.sh _wireguard_add1:wireguard.sh __wireguard_add2:wireguard.sh _wireguard_add3:wireguard.sh _wireguard_link2:wireguard.sh _wireguard_remove1:wireguard.sh _wireguard_remove2:wireguard.sh _wireguard_validate0:wireguard.sh ___wireguard_clean0:wireguard.sh ___wireguard_clean1:wireguard.sh _wireguard_patch1:wireguard.sh _wireguard_info1:wireguard.sh ___wireguard_dump0:wireguard.sh __wireguard_help0:wireguard.sh"
# === END AUTO-GENERATED ===
# vpn-switch Unit Test Suite - Formalized Edition
#
# Principles:
# - One systematic way, no exceptions
# - Every test uses 'vpn-switch bootstrap' for database creation
# - test_setup() does minimal, universal setup only
# - Each test is self-contained and tailors its own environment
# - No cleanup - leave artifacts for debugging
# - Per-test tracing and output capture
#
# POSIX shell compliant
#
set -e
set -u
#-----------------------------------------------------------------------------
# Test Configuration (Prologue)
#-----------------------------------------------------------------------------
# Test base directory - evaluated once at script start
# Uses TMPDIR if set, otherwise /tmp (making TMPDIR dependency explicit)
TEST_BASE_DIR="${TMPDIR:-/tmp}/vpn-switch-test.$$"
# Test script location
TEST_SCRIPT="./vpn-switch.sh"
# Profile for bootstrap (can be overridden via command-line argument)
# Default: minimal (only essential variables)
# Alternative: all (all .env-templates installed - quality gate for tests)
#
# Test filtering (optional): Specify test function names to run only those tests
#
# Usage:
# ./vpn-switch-unit-test.sh # minimal profile, all tests
# ./vpn-switch-unit-test.sh all # all profile, all tests
# ./vpn-switch-unit-test.sh minimal test_batch_validate_includes_system
# ./vpn-switch-unit-test.sh all test_session_list test_session_show
# Options (before the positional args): --maxprocs N runs the top-level
# test functions in parallel (xargs -P N, each in its own suite process and
# sandbox); default 1 = the classic sequential run. (Ported from elebake.)
MAXPROCS=1
while [ $# -gt 0 ]; do
case "$1" in
--maxprocs) MAXPROCS="${2:?--maxprocs needs a value}"; shift 2 ;;
--maxprocs=*) MAXPROCS="${1#--maxprocs=}"; shift ;;
*) break ;;
esac
done
TEST_PROFILE="${1:-minimal}"
[ $# -ge 1 ] && shift
# Dynamically generate list of all test functions
ALL_TESTS=$(grep -o '^test_[a-z_]*()' "$0" | sed 's/()$//' | tr '\n' ' ')
# Collect test filter arguments (optional test function names)
TEST_FILTER="$*"
# If no filter specified, run all tests
if [ -z "$TEST_FILTER" ]; then
TEST_FILTER="$ALL_TESTS"
fi
# Source platform-specific commands (EXAMINE_*, MODIFY_*)
# Uses the default symlink which points to the correct platform
PLATFORM_FILE="./template/platform/default"
if [ -f "$PLATFORM_FILE" ]; then
. "$PLATFORM_FILE"
else
echo "ERROR: Platform file not found: $PLATFORM_FILE" >&2
echo "Run this script from the repository root" >&2
exit 1
fi
#-----------------------------------------------------------------------------
# Test Framework Infrastructure
#-----------------------------------------------------------------------------
# should_run_test - Run test if it matches the filter
#
# Args: $1 - test function name (e.g., "test_batch_validate_includes_system")
#
# Returns: Always 0 (to prevent set -e from exiting when test is skipped)
#
# Usage:
# should_run_test test_session_list
#
should_run_test() {
local test_name="$1"
# Check if test_name is in the filter list
for filter_test in $TEST_FILTER; do
if [ "$filter_test" = "$test_name" ]; then
"$test_name"
return 0
fi
done
# Test not in filter list - skip it
return 0
}
# Test counters
TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0
# Color support - always use escape codes
# Using literal escape codes for portability (tput output varies across systems)
# All color output uses printf '%b' to interpret escape sequences
# Note: Always write escape codes so log files can be viewed with color
# using tools like 'tail -f', 'less -R', or terminal emulators
COLOR_GREEN='\033[1;32m'
COLOR_RED='\033[1;31m'
COLOR_YELLOW='\033[1;33m'
COLOR_BLUE='\033[1;34m'
COLOR_RESET='\033[0m'
# Global test directory (set by test_setup, used by all tests)
TEST_DIR=""
# pass - Mark assertion as passed
pass() {
TESTS_PASSED=$((TESTS_PASSED + 1))
printf '%b✓%b %s\n' "$COLOR_GREEN" "$COLOR_RESET" "$*"
}
# fail - Mark assertion as failed
fail() {
TESTS_FAILED=$((TESTS_FAILED + 1))
printf '%b✗%b %s\n' "$COLOR_RED" "$COLOR_RESET" "$*"
}
# get_file_perms - Get file permissions in octal (cross-platform)
# Uses EXAMINE_FILE_PERMS from platform script (e.g., "stat -f %Lp" on FreeBSD)
get_file_perms() {
local file="$1"
$EXAMINE_FILE_PERMS "$file" 2>/dev/null
}
# get_file_perms_full - Get file permissions in octal (cross-platform)
# Uses EXAMINE_FILE_PERMS from platform script
# Returns just permission bits (e.g., "600") - tests accept both "100600" and "600" formats
get_file_perms_full() {
local file="$1"
$EXAMINE_FILE_PERMS "$file" 2>/dev/null
}
# test_header - Print test header
#
# Args: $1 - Test description
# $2 - Optional: Function name (for easy test selection)
#
test_header() {
local description="$1"
local function_name="${2:-}"
TESTS_RUN=$((TESTS_RUN + 1))
echo ""
if [ -n "$function_name" ]; then
printf '%bTEST %s (%s):%b %s\n' "$COLOR_BLUE" "$TESTS_RUN" "$function_name" "$COLOR_RESET" "$description"
else
printf '%bTEST %s:%b %s\n' "$COLOR_BLUE" "$TESTS_RUN" "$COLOR_RESET" "$description"
fi
}
# test_summary - Print test summary
test_summary() {
local gh
for gh in /tmp/vsut-gh-$$-*; do [ -d "$gh" ] && GNUPGHOME="$gh" gpgconf --kill all 2>/dev/null && rm -rf "$gh"; done
local test_functions=$TESTS_RUN
local assertions_passed=$TESTS_PASSED
local assertions_failed=$TESTS_FAILED
local total_assertions=$((assertions_passed + assertions_failed))
echo ""
echo "========================================"
echo "Test Summary"
echo "========================================"
echo "Test Functions: $test_functions"
echo "Total Assertions: $total_assertions"
printf '%bPassed Assertions: %s%b\n' "$COLOR_GREEN" "$assertions_passed" "$COLOR_RESET"
if [ "$assertions_failed" -gt 0 ]; then
printf '%bFailed Assertions: %s%b\n' "$COLOR_RED" "$assertions_failed" "$COLOR_RESET"
else
echo "Failed Assertions: $assertions_failed"
fi
echo ""
if [ "$assertions_failed" -gt 0 ]; then
printf '%bSOME TESTS FAILED%b\n' "$COLOR_RED" "$COLOR_RESET"
echo ""
echo "Test artifacts preserved in: $TEST_BASE_DIR"
echo "To clean up: rm -rf $TEST_BASE_DIR"
return 1
else
printf '%bALL TESTS PASSED%b\n' "$COLOR_GREEN" "$COLOR_RESET"
return 0
fi
}
#-----------------------------------------------------------------------------
# Universal Test Setup (Minimal, Systematic, No Exceptions)
#-----------------------------------------------------------------------------
# test_setup - Create EXCLUSIVE test database for THIS test
#
# Principles:
# - Each test gets its own exclusive database directory
# - Uses 'vpn-switch bootstrap' to create database (systematic, no exceptions)
# - bootstrap creates directory and initializes database in one command
# - NO test-specific configuration (tests do that themselves)
#
# Returns: Sets TEST_DIR to the exclusive database directory for this test
#
test_setup() {
# Create exclusive test directory for THIS test
local test_num=$TESTS_RUN
TEST_DIR="$TEST_BASE_DIR/test_${test_num}"
# Show database path for debugging
printf ' %bDatabase:%b %s\n' "$COLOR_BLUE" "$COLOR_RESET" "$TEST_DIR"
# Run 'vpn-switch bootstrap' to create database
# Bootstrap creates the directory and initializes the database structure
# This is the ONLY way we create test databases (systematic, no exceptions)
# Profile comes from TEST_PROFILE (set via command-line argument or default)
mkdir -p "$TEST_BASE_DIR"
local bootstrap_log="$TEST_BASE_DIR/test_${test_num}.bootstrap.log"
# Set minimal PATH for bootstrap operations (needed for run_env before database exists)
export VPN_SWITCH_PATH="/bin:/usr/bin:/usr/local/bin"
if ! "$TEST_SCRIPT" bootstrap "$TEST_DIR" "$TEST_PROFILE" > "$bootstrap_log" 2>&1; then
printf '%bERROR:%b vpn-switch bootstrap failed for test %s (profile: %s)\n' "$COLOR_RED" "$COLOR_RESET" "$test_num" "$TEST_PROFILE"
printf '%bBootstrap output:%b\n' "$COLOR_YELLOW" "$COLOR_RESET"
cat "$bootstrap_log" | sed 's/^/ /'
echo ""
echo "Full log saved to: $bootstrap_log"
exit 1
fi
# Simulate experienced user who overrides safety default
# Both minimal and all profiles ship TERMINAL_INTERPRETER=cat (safe by default)
# Tests simulate user who has opted into auto-execution for convenience
# Use setenv to configure (black-box approach - tests the actual user interface)
VPN_SWITCH_BASE="$TEST_DIR" "$TEST_SCRIPT" setenv VPN_SWITCH_TERMINAL_INTERPRETER sh > /dev/null 2>&1
VPN_SWITCH_BASE="$TEST_DIR" "$TEST_SCRIPT" setenv VPN_SWITCH_INTERPRETER_interface_destroy cat > /dev/null 2>&1
# Set minimal PATH for vpn-switch operations
VPN_SWITCH_BASE="$TEST_DIR" "$TEST_SCRIPT" setenv VPN_SWITCH_PATH "/bin:/usr/bin:/usr/local/bin" > /dev/null 2>&1
# Copy test configs into database (if available)
if [ -d "test-configs/wireguard" ]; then
cp test-configs/wireguard/*.conf "$TEST_DIR/wireguard/" 2>/dev/null || true
chmod 0400 "$TEST_DIR/wireguard"/*.conf 2>/dev/null || true
fi
}
# setup_session_test_mocks - Mock external commands for session operation tests
#
# Layer 2/3 tests (start, connect, stop, session operations) need to mock
# external commands to avoid actually executing network operations.
#
# Args: $1 - Optional test ID for unique interface names (e.g., "13", "14")
# If provided, interfaces will be named wg_test<ID>, tun_test<ID>
# If omitted, uses default wg9, tun9 (backward compatible)
#
# Uses .env/local/ hierarchy (proper override location) to mock:
# - VPN_SWITCH_BINARY_wireguard: wg-quick → echo wg-quick (mock network interface ops)
# - VPN_SWITCH_PFCTL: pfctl → echo pfctl (mock firewall ops)
# - VPN_SWITCH_INSTALL: install → echo install (mock file installation ops)
# - VPN_SWITCH_WG_INTERFACE: wg0 → wg_test<ID> (unique test interface name)
#
# Must be called AFTER test_setup() for tests that need command mocking.
#
setup_session_test_mocks() {
local test_id="${1:-9}" # Default to 9 for backward compatibility
# Determine interface names based on test_id
local wg_interface="wg_test${test_id}"
local ovpn_interface="tun_test${test_id}"
# Mock wg-quick to avoid actually creating network interfaces
run_vpn_switch setenv VPN_SWITCH_BINARY_wireguard "echo wg-quick" > /dev/null 2>&1
# Mock openvpn to avoid actually creating VPN tunnels
run_vpn_switch setenv VPN_SWITCH_BINARY_openvpn "echo openvpn" > /dev/null 2>&1
# Mock pfctl to avoid actually modifying firewall rules
run_vpn_switch setenv VPN_SWITCH_PFCTL "echo pfctl" > /dev/null 2>&1
# Mock install to avoid actually writing to system directories
run_vpn_switch setenv VPN_SWITCH_INSTALL "echo install" > /dev/null 2>&1
# Use unique test interface names to avoid conflicts between tests
run_vpn_switch setenv VPN_SWITCH_INTERFACE_wireguard "$wg_interface" > /dev/null 2>&1
run_vpn_switch setenv VPN_SWITCH_INTERFACE_openvpn "$ovpn_interface" > /dev/null 2>&1
# Set resolvconf script paths (base path, scripts append -up/-down)
run_vpn_switch setenv VPN_SWITCH_RESOLVCONF_wireguard "/usr/local/sbin/wg-resolvconf" > /dev/null 2>&1
run_vpn_switch setenv VPN_SWITCH_RESOLVCONF_openvpn "/usr/local/sbin/ovpn-resolvconf" > /dev/null 2>&1
}
#-----------------------------------------------------------------------------
# Helper Functions
#-----------------------------------------------------------------------------
# set_function_interpreter - Set interpreter for a specific function
#
# Reduces code duplication by centralizing the pattern of setting function-specific
# interpreters in .env/local/ directory.
#
# Args: $1 - Function name (e.g., "wireguard_connect", "session_list")
# $2 - Interpreter value (e.g., "cat", "sh", "echo")
#
# Creates: $TEST_DIR/.env/local/VPN_SWITCH_INTERPRETER_<function>
#
# Usage:
# set_function_interpreter "wireguard_connect" "cat"
# set_function_interpreter "session_list" "sh"
#
set_function_interpreter() {
local func_name="$1"
local interpreter="$2"
# Use setenv to configure interpreter (black-box approach)
run_vpn_switch setenv "VPN_SWITCH_INTERPRETER_${func_name}" "$interpreter" > /dev/null 2>&1
}
# override_for_command_inspection - Override interpreter for command inspection
#
# Tests that need to inspect command output (grep for specific commands) must
# explicitly override the interpreter to 'cat' regardless of profile.
#
# Args: $1 - Function name (without underscore prefix)
#
# This helper ensures tests are profile-independent by setting function-specific
# interpreter (highest priority in resolution chain), which overrides intrinsic
# classification (TERMINAL_INTERPRETER).
#
# Usage:
# test_setup
# override_for_command_inspection "link"
# output=$(VPN_SWITCH_BASE="$TEST_DIR" "$TEST_SCRIPT" link testproto wireguard)
# grep "ln -s" <<< "$output" # Works with ANY profile
#
override_for_command_inspection() {
local func="$1"
if [ -z "$func" ]; then
printf '%bERROR:%b override_for_command_inspection requires function name\n' "$COLOR_RED" "$COLOR_RESET"
exit 1
fi
# Set function-specific interpreter to cat (highest priority)
# This overrides both TERMINAL_INTERPRETER and intrinsic classification
set_function_interpreter "$func" "cat"
}
# run_vpn_switch - Execute vpn-switch.sh through the user interface
#
# Arguments:
# $1-N: command arguments to pass to vpn-switch.sh
#
# Environment:
# Uses only variables a normal user would set:
# - VPN_SWITCH_BASE: database location (always set in tests)
# - VPN_SWITCH_DISPLAY_ANSI: disable colors for test parsing
#
# Log/trace files are automatically created in database's .log/ directory
# based on LOG_RETENTION_DAYS and TRACE_RETENTION_DAYS settings (default: 1 day)
#
# Sets up per-test output capture:
# - Output saved to: $TEST_BASE_DIR/traces/output_${TESTS_RUN}.log
#
# Returns: captured stdout and stderr
#
run_vpn_switch() {
# Create traces directory for test output capture
local test_num=$TESTS_RUN
local traces_dir="$TEST_BASE_DIR/traces"
mkdir -p "$traces_dir"
# Set up per-test output file for assertions
local output_file="$traces_dir/output_${test_num}.log"
# Execute vpn-switch exactly as a user would
# Only user-facing environment variables:
# - VPN_SWITCH_BASE: where the database lives
# - VPN_SWITCH_DISPLAY_ANSI: disable colors for grep parsing
# Note: Log/trace files are automatically managed by retention settings
VPN_SWITCH_BASE="$TEST_DIR" \
VPN_SWITCH_DISPLAY_ANSI=0 \
"$TEST_SCRIPT" "$@" 2>&1 | tee "$output_file"
}
# unit_attest_key <record> -- a throwaway OpenPGP key in a home short enough
# for gpg-agent's socket, registered in the test database and pinned as the
# archive attest key. Sets UNIT_GNUPGHOME and UNIT_FPR; returns 1 (and
# passes with a note) when gpg is absent. (Ported from elebake.)
unit_attest_key() {
command -v gpg > /dev/null 2>&1 || { pass "gpg absent -- signing checks skipped"; return 1; }
UNIT_GNUPGHOME="/tmp/vsut-gh-$$-$TESTS_RUN"
rm -rf "$UNIT_GNUPGHOME"; mkdir -p "$UNIT_GNUPGHOME" && chmod 700 "$UNIT_GNUPGHOME"
GNUPGHOME="$UNIT_GNUPGHOME" gpg --batch --passphrase '' --pinentry-mode loopback \
--quick-generate-key "Unit $TESTS_RUN <unit@example.invalid>" rsa2048 sign never > /dev/null 2>&1
UNIT_FPR=$(GNUPGHOME="$UNIT_GNUPGHOME" gpg --list-secret-keys --with-colons 2>/dev/null | awk -F: '/^fpr:/{print $10; exit}')
[ -n "$UNIT_FPR" ] || { fail "could not create a test key (gpg-agent socket path too long?)"; return 1; }
run_vpn_switch openpgp add "$1" "$UNIT_FPR" "$UNIT_GNUPGHOME" > /dev/null 2>&1
run_vpn_switch setenv VPN_SWITCH_ARCHIVE_ATTEST_KEY "$1" > /dev/null 2>&1
return 0
}
test_restore_pin_in_prologue() {
test_header "restore: a pin in the prologue governs the lines after environment refresh in the same replay" "test_restore_pin_in_prologue"
test_setup
unit_attest_key unit || return 0
local dump="$TEST_BASE_DIR/unit-restore-pin-$TESTS_RUN.sh" c="$TEST_BASE_DIR/restore-pin-$TESTS_RUN.conf" rc=0 out
printf '[Interface]\nPrivateKey = x\nAddress = 10.0.0.2/32\n[Peer]\nPublicKey = y\nEndpoint = 1.2.3.4:51820\nAllowedIPs = 0.0.0.0/0\n' > "$c"
printf '# vpn-switch database dump\n# Version: 1\n# Serial: 1\n' > "$dump"
printf '%s\n' '"$VPN_SWITCH_CONTEXT_SCRIPT" setenv VPN_SWITCH_INTERPRETER_wireguard_import1 cat' \
'"$VPN_SWITCH_CONTEXT_SCRIPT" environment refresh' \
"\"\$VPN_SWITCH_CONTEXT_SCRIPT\" wireguard import $c" >> "$dump"
run_vpn_switch attest "$dump" unit > /dev/null 2>&1
out=$(run_vpn_switch restore "$dump" 2>&1) || rc=$?
if [ "$rc" -eq 0 ] && [ "$(head -1 "$TEST_DIR/.env/local/VPN_SWITCH_INTERPRETER_wireguard_import1" 2>/dev/null)" = cat ] \
&& [ ! -f "$TEST_DIR/wireguard/${c##*/}" ] && printf '%s\n' "$out" | grep -q "^# Import WireGuard config: ${c##*/}"; then
pass "the pin acted, the import after the refresh was displayed, the replay succeeded"
else
fail "restore with a prologue pin: rc=$rc imported=$(ls "$TEST_DIR/wireguard" 2>/dev/null | tr '\n' ' ') $(printf '%s\n' "$out" | grep -i 'error' | head -2 | tr '\n' ' ')"
fi
}
test_export_import_pair() {
test_header "export/import: the signed pair moves the database -- dump + bundle, sealed, pinned signer, receipt, serial floor" "test_export_import_pair"
test_setup
unit_attest_key unit || return 0
local src="$TEST_DIR" dst="$TEST_BASE_DIR/pair-dst-$TESTS_RUN" out="$TEST_BASE_DIR/pair-out-$TESTS_RUN" o
mkdir -p "$out"
printf '[Interface]\nPrivateKey = x\nAddress = 10.0.0.2/32\n[Peer]\nPublicKey = y\nEndpoint = 1.2.3.4:51820\nAllowedIPs = 0.0.0.0/0\n' > "$out/ch-zrh-01.conf"
run_vpn_switch wireguard import "$out/ch-zrh-01.conf" > /dev/null 2>&1
run_vpn_switch wireguard add privacy > /dev/null 2>&1
run_vpn_switch wireguard add privacy ch-zrh-01.conf > /dev/null 2>&1
if run_vpn_switch dump 2>/dev/null | grep -q '^# Version: 1$' \
&& run_vpn_switch dump 2>/dev/null | grep -q 'wireguard import "\$VPN_SWITCH_ARCHIVE_BASE/wireguard/ch-zrh-01.conf"'; then
pass "the dump carries the format version and names its base elements against the archive base"
else
fail "dump header/paths wrong: $(run_vpn_switch dump 2>/dev/null | grep -n 'Version\|wireguard import')"
fi
o=$(run_vpn_switch export full "$out/vs.sh" "$out/vs.tar.gz" 2>&1) || true
if [ -f "$out/vs.sh" ] && [ -f "$out/vs.sh.asc" ] && [ -f "$out/vs.tar.gz" ] \
&& grep -q '^# Bundle: sha256=' "$out/vs.sh" && [ "$(cat "$src/export/serial")" = 1 ]; then
pass "export writes dump, signature and bundle, seals the pair and advances the serial to 1"
else
fail "export incomplete: $o"
fi
if tar -tzf "$out/vs.tar.gz" | grep -q 'wireguard/ch-zrh-01.conf' && tar -tzf "$out/vs.tar.gz" | grep -q 'export/MANIFEST.asc'; then
pass "the bundle carries the configuration and the attested MANIFEST"
else
fail "bundle content wrong: $(tar -tzf "$out/vs.tar.gz")"
fi
run_vpn_switch export redacted "$out/red.sh" "$out/red.tar.gz" > /dev/null 2>&1
if [ -f "$out/red.tar.gz" ] && ! tar -tzf "$out/red.tar.gz" | grep -q 'wireguard/ch-zrh-01.conf' \
&& grep -q "dropped (secret).*ch-zrh-01.conf" "$TEST_DIR/export/collection" \
&& grep -q "wireguard add privacy" "$out/red.sh"; then
pass "export redacted: the description travels complete, the configuration (the secret) stays home, auditable in the collection"
else
fail "redacted export wrong: $(tar -tzf "$out/red.tar.gz" 2>&1; grep dropped "$TEST_DIR/export/collection")"
fi
run_vpn_switch export minimized "$out/min.sh" "$out/min.tar.gz" > /dev/null 2>&1
if tar -tzf "$out/min.tar.gz" | grep -q 'wireguard/ch-zrh-01.conf' && tar -tzf "$out/min.tar.gz" | grep -q 'openpgp/unit/keyid'; then
pass "export minimized: the configurations and the key travel"
else
fail "minimized export wrong: $(tar -tzf "$out/min.tar.gz" 2>&1)"
fi
if run_vpn_switch export bogus "$out/x.sh" "$out/x.tar.gz" 2>&1 | grep -q "unknown strategy bogus"; then
pass "an unknown strategy is refused by name"
else
fail "unknown strategy accepted"
fi
# the receiving database: fresh, the sender's key pinned
VPN_SWITCH_PATH="/bin:/usr/bin:/usr/local/bin" "$TEST_SCRIPT" bootstrap "$dst" "$TEST_PROFILE" > /dev/null 2>&1 || true
VPN_SWITCH_BASE="$dst" "$TEST_SCRIPT" setenv VPN_SWITCH_TERMINAL_INTERPRETER sh > /dev/null 2>&1
VPN_SWITCH_BASE="$dst" "$TEST_SCRIPT" setenv VPN_SWITCH_INTERPRETER_interface_destroy cat > /dev/null 2>&1
o=$(VPN_SWITCH_BASE="$dst" VPN_SWITCH_DISPLAY_ANSI=0 "$TEST_SCRIPT" import "$out/vs.sh" "$out/vs.tar.gz" 2>&1) || true
if printf '%s\n' "$o" | grep -q "ATTEST_KEY not set" && [ ! -f "$dst/wireguard/ch-zrh-01.conf" ]; then
pass "import without a pinned signer is refused before anything lands"
else
fail "import without a pin: $o"
fi
VPN_SWITCH_BASE="$dst" "$TEST_SCRIPT" openpgp add unit "$UNIT_FPR" "$UNIT_GNUPGHOME" > /dev/null 2>&1
VPN_SWITCH_BASE="$dst" "$TEST_SCRIPT" setenv VPN_SWITCH_ARCHIVE_ATTEST_KEY unit > /dev/null 2>&1
o=$(VPN_SWITCH_BASE="$dst" VPN_SWITCH_DISPLAY_ANSI=0 "$TEST_SCRIPT" import "$out/vs.sh" "$out/vs.tar.gz" 2>&1) || true
if [ -f "$dst/wireguard/ch-zrh-01.conf" ] && [ -L "$dst/wireguard/privacy/ch-zrh-01.conf" ] \
&& [ "$(cat "$dst"/provenance/*/serial 2>/dev/null)" = 1 ] && [ "$(cat "$dst"/provenance/*/signer 2>/dev/null)" = "$UNIT_FPR" ] \
&& [ "$(cat "$dst/export/serial")" = 1 ]; then
pass "import rebuilt the configuration and the category in a fresh database, filed the receipt and continued the lineage"
else
fail "import result wrong: $o $(ls -R "$dst/wireguard" "$dst/provenance" 2>&1 | tr '\n' ' ')"
fi
o=$(VPN_SWITCH_BASE="$dst" VPN_SWITCH_DISPLAY_ANSI=0 "$TEST_SCRIPT" import "$out/vs.sh" "$out/vs.tar.gz" 2>&1) || true
if printf '%s\n' "$o" | grep -q "already filed" && printf '%s\n' "$o" | grep -q "Config already exists"; then
pass "the same pair again: the receipt stands, the replay reports what is there and goes on"
else
fail "re-import wrong: $o"
fi
# a database that is not there: import bootstraps it from the dump (its profile, the signer from the keyring the record names)
local fresh="$TEST_BASE_DIR/pair-fresh-$TESTS_RUN/db"
if grep -q "^# Profile: $TEST_PROFILE\$" "$out/vs.sh"; then
pass "the dump header names the profile of its database"
else
fail "no profile line: $(head -n 6 "$out/vs.sh" | tr '\n' ' ')"
fi
o=$(VPN_SWITCH_BASE="$fresh" VPN_SWITCH_DISPLAY_ANSI=0 "$TEST_SCRIPT" import "$out/vs.sh" "$out/vs.tar.gz" 2>&1) || true
if [ -f "$fresh/wireguard/ch-zrh-01.conf" ] && [ "$(cat "$fresh/openpgp/unit/keyid" 2>/dev/null)" = "$UNIT_FPR" ] \
&& [ "$(cat "$fresh/.env/local/VPN_SWITCH_ARCHIVE_ATTEST_KEY" 2>/dev/null)" = unit ] \
&& [ "$(cat "$fresh"/provenance/*/signer 2>/dev/null)" = "$UNIT_FPR" ]; then
pass "import bootstrap: the database made with the dump's profile, the signer pinned under the dump's record name, the dump replayed, the receipt filed"
else
fail "import bootstrap: $(printf '%s\n' "$o" | grep -i error | head -3 | tr '\n' ' ') $(ls "$fresh" 2>&1 | tr '\n' ' ')"
fi
sed '/^# Profile:/d' "$out/vs.sh" > "$out/noprofile.sh"; cp "$out/vs.sh.asc" "$out/noprofile.sh.asc"
o=$(VPN_SWITCH_BASE="$fresh-2" VPN_SWITCH_DISPLAY_ANSI=0 "$TEST_SCRIPT" import "$out/noprofile.sh" "$out/vs.tar.gz" 2>&1) || true
if printf '%s\n' "$o" | grep -q "names no profile" && [ ! -d "$fresh-2/.env" ]; then
pass "a dump without a profile line cannot bootstrap: refused, nothing made"
else
fail "no-profile dump: $o"
fi
run_vpn_switch export full "$out/vs2.sh" "$out/vs2.tar.gz" > /dev/null 2>&1
VPN_SWITCH_BASE="$dst" "$TEST_SCRIPT" import "$out/vs2.sh" "$out/vs2.tar.gz" > /dev/null 2>&1 || true
o=$(VPN_SWITCH_BASE="$dst" VPN_SWITCH_DISPLAY_ANSI=0 "$TEST_SCRIPT" import "$out/vs.sh" "$out/vs.tar.gz" 2>&1) || true
if printf '%s\n' "$o" | grep -q "DOWNGRADE" && [ "$(cat "$dst/export/serial")" = "$(sed -n 's/^# Serial: //p' "$out/vs2.sh")" ]; then
pass "after the later serial the validly signed serial-1 pair is refused as a downgrade"
else
fail "downgrade accepted: $o"
fi
cp "$out/vs2.sh" "$out/edited.sh"; cp "$out/vs2.sh.asc" "$out/edited.sh.asc"; printf '# x\n' >> "$out/edited.sh"
o=$(VPN_SWITCH_BASE="$dst" VPN_SWITCH_DISPLAY_ANSI=0 "$TEST_SCRIPT" import "$out/edited.sh" "$out/vs2.tar.gz" 2>&1) || true
if printf '%s\n' "$o" | grep -q "BAD SIGNATURE"; then
pass "an edited dump is refused by its signature"
else
fail "tampered dump accepted: $o"
fi
printf 'x' >> "$out/vs2.tar.gz"
o=$(VPN_SWITCH_BASE="$dst" VPN_SWITCH_DISPLAY_ANSI=0 "$TEST_SCRIPT" import "$out/vs2.sh" "$out/vs2.tar.gz" 2>&1) || true
if printf '%s\n' "$o" | grep -q "MISMATCHED PAIR"; then
pass "a bundle the dump does not name is refused by the seal"
else
fail "foreign bundle accepted: $o"
fi
run_vpn_switch dump > "$out/plain.sh" 2>/dev/null
if run_vpn_switch restore "$out/plain.sh" 2>&1 | grep -q "unsigned" \
&& run_vpn_switch restore "$out/vs2.sh" 2>&1 | grep -q "already exists"; then
pass "restore takes the same admissibility path as import: an unsigned dump is refused, the attested one replays"
else
fail "restore path wrong: $(run_vpn_switch restore "$out/plain.sh" 2>&1 | head -2)"
fi
o=$(run_vpn_switch export full "$out/vs2.sh" "$out/vs2.tar.gz" 2>&1) || true
if [ -f "$out/vs2.sh.asc" ] && ! printf '%s\n' "$o" | grep -q "already attested" && grep -q '^# Bundle: sha256=' "$out/vs2.sh"; then
pass "a second export into the same paths succeeds -- the earlier signature is void, not an obstacle"
else
fail "second export: $(printf '%s\n' "$o" | grep -i 'error' | head -2 | tr '\n' ' ')"
fi
rm -rf "$UNIT_GNUPGHOME"
}
# validate_batch_syntax - Validate batch combinator output syntax (Issue #001)
#
# Args: $1 - output (batch combinator output to validate)
# $2 - function_name (for error messages)
#
# Returns: 0 if valid, 1 if invalid (with error message)
#
# Validates that batch combinator output follows strict syntax rules:
# - Non-comment lines must start with "$VPN_SWITCH_CONTEXT_SCRIPT"
# - Variable must be literal (escaped $), not expanded
# - No multiple commands per line (no ;, &&, ||)
# - Comments and empty lines are allowed
#
validate_batch_syntax() {
local output="$1"
local function_name="$2"
local line_num=0
echo "$output" | while IFS= read -r line; do
line_num=$((line_num + 1))
# Skip empty lines
[ -z "$line" ] && continue
# Allow comments
case "$line" in
'#'*) continue ;;
esac
# Must start with literal "$VPN_SWITCH_CONTEXT_SCRIPT"
if ! echo "$line" | grep -q '^"\$VPN_SWITCH_CONTEXT_SCRIPT"'; then
echo "ERROR: $function_name line $line_num: Must start with \"\\\$VPN_SWITCH_CONTEXT_SCRIPT\""
echo " Got: $line"
return 1
fi
# Must NOT have expanded path (absolute paths indicate expansion)
if echo "$line" | grep -qE '"/[^$].*vpn-switch'; then
echo "ERROR: $function_name line $line_num: Variable expanded (not literal)"
echo " Got: $line"
return 1
fi
# Must NOT have multiple commands per line
if echo "$line" | grep -qE ';|&&|\|\|'; then
echo "ERROR: $function_name line $line_num: Multiple commands per line"
echo " Got: $line"
return 1
fi
done
}
#-----------------------------------------------------------------------------
# Test Cases
#-----------------------------------------------------------------------------
# TEST 1: Basic printenv command (simplest possible test)
test_complete_command() {
test_header "complete - completion candidates from the help corpus" "test_complete_command"
# Create exclusive database for this test
test_setup
# Top level: the first words of every command path, nothing else
local output=$(run_vpn_switch complete "")
if printf '%s\n' "$output" | grep -qx "wireguard" && printf '%s\n' "$output" | grep -qx "session" \
&& printf '%s\n' "$output" | grep -qx "help" && printf '%s\n' "$output" | grep -qx "complete"; then
pass "Top-level candidates list the command words"
else
fail "Top-level candidates incomplete: $(printf '%s' "$output" | tr '\n' ' ')"
fi
if printf '%s\n' "$output" | grep -q '[<>]'; then
fail "Top-level candidates leak placeholders: $(printf '%s' "$output" | tr '\n' ' ')"
else
pass "No placeholder syntax leaks into candidates"
fi
# Prefix filtering on literal words
output=$(run_vpn_switch complete wireguard st)
if [ "$(printf '%s\n' "$output" | tr '\n' ' ')" = "start stop " ]; then
pass "'wireguard st' completes to start and stop only"
else
fail "'wireguard st' gave: $(printf '%s' "$output" | tr '\n' ' ')"
fi
# Alternatives written as literals in the usage line
output=$(run_vpn_switch complete environment cache "")
if printf '%s\n' "$output" | grep -qx "on" && printf '%s\n' "$output" | grep -qx "off" && printf '%s\n' "$output" | grep -qx "status"; then
pass "'environment cache' offers on|off|status"
else
fail "'environment cache' gave: $(printf '%s' "$output" | tr '\n' ' ')"
fi
# help completes command paths, group ids and topics
output=$(run_vpn_switch complete help "")
if printf '%s\n' "$output" | grep -qx "session" && printf '%s\n' "$output" | grep -qx "sessions" && printf '%s\n' "$output" | grep -qx "environment"; then
pass "'help' completes command words, group ids and topics"
else
fail "'help' gave: $(printf '%s' "$output" | tr '\n' ' ')"
fi
output=$(run_vpn_switch complete help session sh)
if [ "$(printf '%s\n' "$output" | tr '\n' ' ')" = "show " ]; then
pass "'help session sh' completes the command path"
else
fail "'help session sh' gave: $(printf '%s' "$output" | tr '\n' ' ')"
fi
# Placeholders resolve to database contents: import a config, then complete it
set_function_interpreter "wireguard_import1" "sh"
local test_config="$TEST_BASE_DIR/wg-COMPLETE-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_complete
Address = 10.2.0.9/32
[Peer]
PublicKey = test_public_key_complete
AllowedIPs = 0.0.0.0/0
Endpoint = 9.10.11.12:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
output=$(run_vpn_switch complete wireguard patch wg-COMP)
if [ "$(printf '%s\n' "$output" | tr '\n' ' ')" = "wg-COMPLETE-1.conf " ]; then
pass "'wireguard patch' completes an imported config"
else
fail "'wireguard patch wg-COMP' gave: $(printf '%s' "$output" | tr '\n' ' ')"
fi
output=$(run_vpn_switch complete openvpn patch "")
if printf '%s\n' "$output" | grep -q "wg-COMPLETE"; then
fail "'openvpn patch' offers a WireGuard config (override not applied)"
else
pass "'openvpn patch' does not offer WireGuard configs"
fi
# File arguments hand over to the shell
output=$(run_vpn_switch complete wireguard import "")
if [ "$(printf '%s\n' "$output" | tr '\n' ' ')" = "@files " ]; then
pass "'wireguard import' asks the shell for file names"
else
fail "'wireguard import' gave: $(printf '%s' "$output" | tr '\n' ' ')"
fi
# Fixed value sets
output=$(run_vpn_switch complete helpenv PATH "")
if [ "$(printf '%s\n' "$output" | tr '\n' ' ')" = "default local template " ]; then
pass "'helpenv <name>' completes the layer"
else
fail "'helpenv PATH' gave: $(printf '%s' "$output" | tr '\n' ' ')"
fi
rm -f "$test_config"
}
test_printenv_command() {
test_header "printenv - basic environment display" "test_printenv_command"
# Create exclusive database for this test
test_setup
# Run printenv command
local output=$(run_vpn_switch printenv)
# Verify printenv output shows environment variables header
if printf '%s\n' "$output" | grep -q "Environment variables in isolated context"; then
pass "Printenv command executed and shows header"
else
fail "Printenv command did not show expected output"
fi
# Verify it shows sorted environment
if printf '%s\n' "$output" | grep -q "VPN_SWITCH_BASE"; then
pass "Printenv shows VPN_SWITCH_BASE variable"
else
fail "Printenv did not show VPN_SWITCH_BASE variable"
fi
}
# TEST 2: Status command
test_status_command() {
test_header "status - VPN status display" "test_status_command"
# Create exclusive database for this test
test_setup
# Run status command
local output=$(run_vpn_switch status)
# Verify status output shows header
if printf '%s\n' "$output" | grep -q "VPN Status"; then
pass "Status command shows header"
else
fail "Status command did not show header"
fi
# Verify status shows disconnected state (no VPN running)
if printf '%s\n' "$output" | grep -q "Disconnected"; then
pass "Status shows disconnected state"
else
fail "Status did not show disconnected state"
fi
# Verify status checks network interfaces
if printf '%s\n' "$output" | grep -q "Network Interfaces"; then
pass "Status checks network interfaces"
else
fail "Status did not check network interfaces"
fi
}
# TEST 3: WireGuard import - valid config
test_wireguard_import_valid() {
test_header "wireguard import - import valid config" "test_wireguard_import_valid"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard import (narrow override)
set_function_interpreter "wireguard_import1" "sh"
# Create a test config file in temp location
local test_config="$TEST_BASE_DIR/test-import-wg-NL-99.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_private_key_placeholder
Address = 10.2.0.2/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_placeholder
AllowedIPs = 0.0.0.0/0
Endpoint = 1.2.3.4:51820
EOF
chmod 0600 "$test_config"
# Run import command (actually execute it!)
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Verify the file was actually copied to the database
local imported_file="$TEST_DIR/wireguard/test-import-wg-NL-99.conf"
if [ -f "$imported_file" ]; then
pass "Config file imported to database"
else
fail "Config file was not imported"
ls -la "$TEST_DIR/wireguard/" || true
fi
# Verify permissions are 0400 (read-only)
if [ -f "$imported_file" ]; then
local perms=$(get_file_perms "$imported_file")
if [ "$perms" = "400" ]; then
pass "Permissions set to 0400 (read-only)"
else
fail "Permissions incorrect: got $perms, expected 400"
fi
fi
# Cleanup
rm -f "$test_config"
}
# TEST 4: WireGuard add - create category
test_wireguard_add_category() {
test_header "wireguard add - create category with permissions" "test_wireguard_add_category"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard add (narrow override)
set_function_interpreter "wireguard_add" "sh"
# Run add command (actually execute it!)
run_vpn_switch wireguard add test-streaming >/dev/null 2>&1
# Verify category directory was created
local category_dir="$TEST_DIR/wireguard/test-streaming"
if [ -d "$category_dir" ]; then
pass "Category directory created"
else
fail "Category directory was not created"
ls -la "$TEST_DIR/wireguard/" || true
fi
# Verify permissions are 0755 (rwxr-xr-x)
if [ -d "$category_dir" ]; then
local perms=$(get_file_perms "$category_dir")
if [ "$perms" = "755" ]; then
pass "Permissions set to 0755"
else
fail "Permissions incorrect: got $perms, expected 755"
fi
fi
}
# TEST 5: WireGuard link - create symlink
test_wireguard_link_create() {
test_header "wireguard link - create symlink in protocol directory" "test_wireguard_link_create"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard import and link (narrow overrides)
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "wireguard_link2" "sh"
# First, import a config file to link to
local test_config="$TEST_BASE_DIR/wg-TEST-RS-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_for_link
Address = 10.2.0.3/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_for_link
AllowedIPs = 0.0.0.0/0
Endpoint = 5.6.7.8:51820
EOF
chmod 0600 "$test_config"
# Import the config file first
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Now create the link
run_vpn_switch wireguard link test-link wg-TEST-RS-1.conf >/dev/null 2>&1
# Verify symlink was created
local link_path="$TEST_DIR/wireguard/test-link"
if [ -L "$link_path" ]; then
pass "Symlink created"
else
fail "Symlink was not created"
ls -la "$TEST_DIR/wireguard/" || true
fi
# Verify symlink points to the correct target
if [ -L "$link_path" ]; then
local link_target=$(readlink "$link_path")
if [ "$link_target" = "./wg-TEST-RS-1.conf" ]; then
pass "Symlink points to correct relative path (./wg-TEST-RS-1.conf)"
else
fail "Symlink target incorrect: got '$link_target', expected './wg-TEST-RS-1.conf'"
fi
fi
# Cleanup
rm -f "$test_config"
}
# TEST 6: WireGuard list - display configs and categories
test_wireguard_list() {
test_header "wireguard list - display configurations" "test_wireguard_list"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard import and add (narrow overrides)
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "wireguard_add" "sh"
# Add some test data to make the list more interesting
# Import a config
local test_config="$TEST_BASE_DIR/wg-TEST-NL-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_nl1
Address = 10.2.0.4/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_nl1
AllowedIPs = 0.0.0.0/0
Endpoint = 9.10.11.12:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Create a category
run_vpn_switch wireguard add privacy >/dev/null 2>&1
# Run list command
local output=$(run_vpn_switch wireguard list)
# Verify header displayed
if printf '%s\n' "$output" | grep -q "WireGuard Configurations"; then
pass "Configuration section header displayed"
else
fail "Configuration header missing"
printf '%s\n' "Output: $output"
fi
# Verify config files section exists
if printf '%s\n' "$output" | grep -q "Configuration files:"; then
pass "Config files section displayed"
else
fail "Config files section missing"
printf '%s\n' "Output: $output"
fi
# Verify our imported config is listed
if printf '%s\n' "$output" | grep -q "wg-TEST-NL-1.conf"; then
pass "Imported config file listed"
else
fail "Imported config not shown"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 7: WireGuard validate - check configuration integrity
test_wireguard_validate() {
test_header "wireguard validate - validate clean configuration" "test_wireguard_validate"
# Create exclusive database for this test
test_setup
# Run validate on clean database
local output=$(run_vpn_switch wireguard validate)
# Verify validation header
if printf '%s\n' "$output" | grep -q "WireGuard Configuration Validation"; then
pass "Validation header displayed"
else
fail "Validation header missing"
printf '%s\n' "Output: $output"
fi
# Verify no errors reported
if printf '%s\n' "$output" | grep -q "No issues found"; then
pass "No issues found in clean configuration"
else
fail "Unexpected issues reported"
printf '%s\n' "Output: $output"
fi
}
# TEST 8: WireGuard info - display category information
test_wireguard_info() {
test_header "wireguard info - display category information" "test_wireguard_info"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard add and import (narrow overrides)
set_function_interpreter "wireguard_add" "sh"
set_function_interpreter "wireguard_import1" "sh"
# Create privacy category
run_vpn_switch wireguard add privacy >/dev/null 2>&1
# Import a config and link it to the category
local test_config="$TEST_BASE_DIR/wg-TEST-CH-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_ch1
Address = 10.2.0.5/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_ch1
AllowedIPs = 0.0.0.0/0
Endpoint = 13.14.15.16:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Link config to privacy category (category, target, alias)
run_vpn_switch wireguard add privacy wg-TEST-CH-1.conf ch1 >/dev/null 2>&1
# Run info command on privacy category
local output=$(run_vpn_switch wireguard info privacy)
# Verify category header displayed
if printf '%s\n' "$output" | grep -q "Category: privacy"; then
pass "Category header displayed"
else
fail "Category header missing"
printf '%s\n' "Output: $output"
fi
# Verify path displayed
if printf '%s\n' "$output" | grep -q "Path:.*privacy"; then
pass "Category path displayed"
else
fail "Category path missing"
printf '%s\n' "Output: $output"
fi
# Verify contents section exists
if printf '%s\n' "$output" | grep -q "Contents:"; then
pass "Contents section displayed"
else
fail "Contents section missing"
printf '%s\n' "Output: $output"
fi
# Verify symlinks shown with arrow notation
if printf '%s\n' "$output" | grep -q -- "->"; then
pass "Category contents listed with symlink arrows"
else
fail "Contents not shown properly"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 9: WireGuard start - create session and connect
test_wireguard_start_basic() {
test_header "wireguard start - create session and connect to VPN" "test_wireguard_start_basic"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations with unique test ID
local test_id=13
setup_session_test_mocks "$test_id"
local test_interface="wg_test${test_id}"
# Enable execution for wireguard add and import (narrow overrides)
set_function_interpreter "wireguard_add" "sh"
set_function_interpreter "wireguard_import1" "sh"
# Create privacy category
run_vpn_switch wireguard add privacy >/dev/null 2>&1
# Import a config and link it to the category
local test_config="$TEST_BASE_DIR/wg-TEST-DE-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_de1
Address = 10.2.0.6/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_de1
AllowedIPs = 0.0.0.0/0
Endpoint = 17.18.19.20:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
run_vpn_switch wireguard add privacy wg-TEST-DE-1.conf de1 >/dev/null 2>&1
# Run start command
local output=$(run_vpn_switch wireguard start privacy 2>&1 || true)
# Verify session was created
if printf '%s\n' "$output" | grep -qi "session"; then
pass "Session creation logged"
else
fail "Session creation not logged"
printf '%s\n' "Output: $output"
fi
# The guard emits the destroy of the interface unconditionally; the act judges when it runs (bound to cat in every test database: displayed, never run)
if printf '%s\n' "$output" | grep -qF "if $EXAMINE_NETWORK_INTERFACES $test_interface >/dev/null 2>&1; then $(printf "$MODIFY_INTERFACE_DESTROY_FMT" "$test_interface") || exit 1" \
&& ! printf '%s\n' "$output" | grep -q "Auto-destroying"; then
pass "The interface destroy is emitted unconditionally and judged by the act"
else
fail "Interface destroy act: $(printf '%s\n' "$output" | grep -i "interface" | head -3)"
fi
# Verify wg-quick command is in generated connect.sh
local session_dir=$(ls -1d "$TEST_DIR/.session"/* 2>/dev/null | head -1)
if [ -f "$session_dir/connect.sh" ] && grep -q "wg-quick" "$session_dir/connect.sh"; then
pass "wg-quick command in connect.sh"
else
fail "wg-quick command not found in connect.sh"
printf '%s\n' "Output: $output"
fi
# Verify session directory was created
local session_count=$(ls -1d "$TEST_DIR/.session"/* 2>/dev/null | wc -l)
if [ "$session_count" -ge 1 ]; then
pass "Session directory created in .session/"
else
fail "Session directory not found"
ls -la "$TEST_DIR/.session/" || true
fi
# Verify session metadata files exist
local session_dir=$(ls -1d "$TEST_DIR/.session"/* 2>/dev/null | head -1)
if [ -f "$session_dir/protocol" ] && [ -f "$session_dir/pid" ]; then
pass "Session metadata files created"
else
fail "Session metadata files missing"
ls -la "$session_dir" || true
fi
# Verify connect.sh was generated
if [ -f "$session_dir/connect.sh" ]; then
pass "connect.sh script generated"
else
fail "connect.sh script not found"
ls -la "$session_dir" || true
fi
# Verify connect.sh is executable
if [ -x "$session_dir/connect.sh" ]; then
pass "connect.sh is executable"
else
fail "connect.sh is not executable"
ls -la "$session_dir/connect.sh" || true
fi
# Verify connect.sh contains relative path to original
if grep -q "Original config: ../../wireguard" "$session_dir/connect.sh"; then
pass "connect.sh documents original config with relative path"
else
fail "connect.sh missing relative path to original"
grep "Original config:" "$session_dir/connect.sh" || true
fi
# Verify patched config exists (uses test interface name)
if [ -f "$session_dir/${test_interface}.conf" ]; then
pass "Patched config (${test_interface}.conf) created"
else
fail "Patched config not found"
ls -la "$session_dir" || true
fi
# Phase-based approach: PostUp/PostDown hooks should be stripped (DNS handled by DNS phase)
if grep -q "PostUp.*resolvconf" "$session_dir/${test_interface}.conf"; then
fail "Patched config should NOT contain PostUp hook (phase-based DNS)"
grep -i "post" "$session_dir/${test_interface}.conf" || true
else
pass "Patched config correctly strips PostUp hook (phase-based DNS)"
fi
# Verify PostDown hook also stripped
if grep -q "PostDown.*resolvconf" "$session_dir/${test_interface}.conf"; then
fail "Patched config should NOT contain PostDown hook (phase-based DNS)"
grep -i "post" "$session_dir/${test_interface}.conf" || true
else
pass "Patched config correctly strips PostDown hook (phase-based DNS)"
fi
# Keepalive injected (source config has none; default is 25)
if grep -q "^PersistentKeepalive = 25$" "$session_dir/${test_interface}.conf"; then
pass "Patched config injects PersistentKeepalive (default 25)"
else
fail "Patched config should inject 'PersistentKeepalive = 25'"
cat "$session_dir/${test_interface}.conf" || true
fi
# Cleanup: Stop the VPN session to avoid interfering with subsequent tests
set_function_interpreter "wireguard_stop" "sh"
run_vpn_switch wireguard stop >/dev/null 2>&1
# Cleanup
rm -f "$test_config"
}
# TEST 10: WireGuard connect - resume existing session
test_wireguard_connect_resume() {
test_header "wireguard connect - resume existing session by ID" "test_wireguard_connect_resume"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations with unique test ID
local test_id=14
setup_session_test_mocks "$test_id"
local test_interface="wg_test${test_id}"
# Enable execution for wireguard add and import (narrow overrides)
set_function_interpreter "wireguard_add" "sh"
set_function_interpreter "wireguard_import1" "sh"
# Create privacy category and config (same setup as start test)
run_vpn_switch wireguard add privacy >/dev/null 2>&1
local test_config="$TEST_BASE_DIR/wg-TEST-SE-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_se1
Address = 10.2.0.7/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_se1
AllowedIPs = 0.0.0.0/0
Endpoint = 21.22.23.24:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
run_vpn_switch wireguard add privacy wg-TEST-SE-1.conf se1 >/dev/null 2>&1
# Run start to create session and extract session ID from output (black-box testing)
local start_output=$(run_vpn_switch wireguard start privacy 2>&1 || true)
# Extract session ID from the output (what the user interface reports)
local session_id=$(echo "$start_output" | grep -oE '[Ss]ession:? [0-9]+|PID:? [0-9]+|session [0-9]+' | grep -oE '[0-9]+' | head -1)
if [ -z "$session_id" ]; then
fail "Could not extract session ID from wireguard start output"
echo "Start output: $start_output"
rm -f "$test_config"
return
fi
# Now test standalone connect with session ID
local output=$(run_vpn_switch wireguard connect "$session_id" 2>&1 || true)
# Verify connect.sh contains expected commands
local session_dir="$TEST_DIR/.session/$session_id"
if [ -f "$session_dir/connect.sh" ] && grep -q "wg-quick" "$session_dir/connect.sh"; then
pass "wireguard connect generated wg-quick command in connect.sh"
else
fail "wg-quick command not found in connect.sh"
printf '%s\n' "Output: $output"
fi
# Verify firewall commands in connect.sh
if grep -q "pfctl" "$session_dir/connect.sh"; then
pass "wireguard connect generated pfctl command in connect.sh"
else
fail "pfctl command not found in connect.sh"
printf '%s\n' "Output: $output"
fi
# Verify connect.sh references the session's config
if grep -qiE "wg9|wireguard" "$session_dir/connect.sh"; then
pass "wireguard connect used session configuration"
else
fail "Session configuration not referenced in connect.sh"
cat "$session_dir/connect.sh" || echo "connect.sh not found"
fi
# Verify connect.sh still exists after connect (shouldn't be deleted)
local session_dir="$TEST_DIR/.session/$session_id"
if [ -f "$session_dir/connect.sh" ]; then
pass "connect.sh preserved after connect"
else
fail "connect.sh was deleted"
ls -la "$session_dir" || true
fi
# Cleanup
rm -f "$test_config"
}
# TEST 11: WireGuard stop - stop active VPN session
test_wireguard_stop_basic() {
test_header "stop - disconnect active VPN session" "test_wireguard_stop_basic"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations
setup_session_test_mocks
# Enable execution for wireguard add, import, and session stop (narrow overrides)
set_function_interpreter "wireguard_add" "sh"
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "wireguard_stop" "sh"
# _batch2 outputs informational messages, not shell commands - override to 'cat'
# Use arity-specific override (batch2) to avoid affecting combinators (__batch0, __batch1)
set_function_interpreter "batch2" "cat"
# Create and start a VPN session (reusing tested functionality)
run_vpn_switch wireguard add privacy >/dev/null 2>&1
local test_config="$TEST_BASE_DIR/wg-TEST-NO-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_no1
Address = 10.2.0.8/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_no1
AllowedIPs = 0.0.0.0/0
Endpoint = 25.26.27.28:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
run_vpn_switch wireguard add privacy wg-TEST-NO-1.conf no1 >/dev/null 2>&1
# Start VPN to create session
run_vpn_switch wireguard start privacy >/dev/null 2>&1
# Verify session exists before stop
local session_count_before=$(ls -1d "$TEST_DIR/.session"/* 2>/dev/null | wc -l)
if [ "$session_count_before" -ge 1 ]; then
pass "Session exists before stop"
else
fail "No session found before stop"
fi
# Verify disconnect.sh exists and contains wg-quick down
local session_dir=$(ls -1d "$TEST_DIR/.session"/* 2>/dev/null | head -1)
if [ -f "$session_dir/disconnect.sh" ] && grep -q "wg-quick.*down" "$session_dir/disconnect.sh"; then
pass "disconnect.sh contains wg-quick down command"
else
fail "wg-quick down command missing from disconnect.sh"
ls -la "$session_dir" || true
fi
# Run stop command
local output=$(run_vpn_switch stop 2>&1 || true)
# Verify disconnect operation logged (or no active sessions if session already stopped)
if printf '%s\n' "$output" | grep -qiE "disconnect|stopping|No.*sessions to stop"; then
pass "Stop command executed"
else
fail "Stop command output unexpected"
printf '%s\n' "Output: $output"
fi
# Verify session was preserved (has 'latest' link after start)
local session_count_after=$(ls -1d "$TEST_DIR/.session"/* 2>/dev/null | wc -l)
if [ "$session_count_after" -eq 1 ]; then
pass "Session preserved after stop (has 'latest' link)"
else
fail "Session not preserved (expected 1, got $session_count_after)"
ls -la "$TEST_DIR/.session/" || true
ls -la "$TEST_DIR/session/" || true
fi
# Cleanup
rm -f "$test_config"
}
# TEST 12: Session list - display active sessions
test_session_list() {
test_header "session list - display active and stale sessions" "test_session_list"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations
setup_session_test_mocks
# Enable execution for wireguard add and import (narrow overrides)
set_function_interpreter "wireguard_add" "sh"
set_function_interpreter "wireguard_import1" "sh"
# Create active session using public API (black-box testing)
run_vpn_switch wireguard add privacy >/dev/null 2>&1
local test_config="$TEST_BASE_DIR/wg-TEST-LIST-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_list1
Address = 10.2.0.10/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_list1
AllowedIPs = 0.0.0.0/0
Endpoint = 10.10.10.10:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
run_vpn_switch wireguard add privacy wg-TEST-LIST-1.conf list1 >/dev/null 2>&1
# Start VPN and extract session ID from output (black-box - use what program tells us)
local start_output=$(run_vpn_switch wireguard start privacy 2>&1 || true)
# Extract session PID from the output (what the user interface reports)
local active_pid=$(echo "$start_output" | grep -oE '[Ss]ession:? [0-9]+|PID:? [0-9]+|session [0-9]+' | grep -oE '[0-9]+' | head -1)
if [ -z "$active_pid" ]; then
fail "Could not extract session ID from wireguard start output"
echo "Start output: $start_output"
return
fi
# Manually create ONLY the stale session (with dead PID) for testing stale detection
# This is the ONLY manual setup needed - we're testing broken/stale session handling
local stale_pid=99997 # Non-existent PID (dead process)
local stale_session="$TEST_DIR/.session/$stale_pid"
mkdir -p "$stale_session"
echo "$stale_pid" > "$stale_session/pid"
echo "wireguard" > "$stale_session/protocol"
echo "$TEST_DIR/wireguard/test-config-2.conf" > "$stale_session/original"
echo "wg_test1" > "$stale_session/interface"
date "+%Y-%m-%d %H:%M:%S" > "$stale_session/started"
echo "99996" > "$stale_session/vpn.pid" # Dead VPN process PID (stale session)
# Run session list
local output=$(run_vpn_switch session list 2>&1 || true)
# Verify header displayed
if printf '%s\n' "$output" | grep -qE "Active VPN Sessions|PID.*PROTOCOL"; then
pass "Session list header displayed"
else
fail "Session list header missing"
printf '%s\n' "Output: $output"
fi
# Verify active session listed
if printf '%s\n' "$output" | grep -q "$active_pid"; then
pass "Active session (PID $active_pid) listed"
else
fail "Active session not listed"
printf '%s\n' "Output: $output"
fi
# Verify stale session listed
if printf '%s\n' "$output" | grep -q "$stale_pid"; then
pass "Stale session (PID $stale_pid) listed"
else
fail "Stale session not listed"
printf '%s\n' "Output: $output"
fi
# Verify stale session marked as stopped (VPN not running)
if printf '%s\n' "$output" | grep "$stale_pid" | grep -q "stopped"; then
pass "Stale session (dead process) marked as stopped"
else
fail "Stale session not marked as stopped"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$test_config"
}
# TEST: session refresh - removes stale ownership for a down interface, idempotent
#
# Scope note: the peer-fingerprint *matching* core (session_peer_matches_interface
# and refresh's re-point-to-matching-session path) cannot be exercised through this
# black-box CLI harness, because the live `wg show` output cannot be stubbed:
# EXAMINE_* variables are re-sourced from the platform file at startup (overriding
# setenv), and PATH injection does not reach the examination subshell. That core
# logic is covered deterministically by vpn-switch-ownership-test.sh. Here we cover
# the harness-reachable plumbing: a stale ownership link for a down/unknown
# interface is removed, and refresh is idempotent.
test_session_refresh() {
test_header "session refresh - removes stale ownership, idempotent" "test_session_refresh"
test_setup
set_function_interpreter "session_refresh" "sh"
# Simulate ownership drift: latest-<iface> points at a dead session whose
# interface is down. Minimal manual broken-state setup (as in test_session_list);
# drift cannot be produced through the public API.
local iface="wg_drift9"
local dead=99931
mkdir -p "$TEST_DIR/.session/$dead" "$TEST_DIR/session"
echo "wireguard" > "$TEST_DIR/.session/$dead/protocol"
echo "$iface" > "$TEST_DIR/.session/$dead/interface"
printf 'PublicKey = drift_key=\n' > "$TEST_DIR/.session/$dead/$iface.conf"
ln -sfn "../.session/$dead" "$TEST_DIR/session/latest-$iface"
local out=$(run_vpn_switch session refresh 2>&1 || true)
if [ ! -L "$TEST_DIR/session/latest-$iface" ]; then
pass "Stale ownership removed for down interface ($iface)"
else
fail "Stale ownership link not removed"
printf '%s\n' "$out"
fi
if printf '%s\n' "$out" | grep -qE "Reconciled ownership|removing stale ownership"; then
pass "Refresh reported the reconciliation"
else
fail "Refresh did not report reconciliation"
printf '%s\n' "$out"
fi
# Idempotent: a second run reports no changes.
out=$(run_vpn_switch session refresh 2>&1 || true)
if printf '%s\n' "$out" | grep -qE "no changes|already consistent"; then
pass "session refresh is idempotent (no-op on second run)"
else
fail "session refresh not idempotent"
printf '%s\n' "$out"
fi
}
# TEST: session refresh - graceful no-op on a clean database
test_session_refresh_clean() {
test_header "session refresh - clean database is a no-op" "test_session_refresh_clean"
test_setup
set_function_interpreter "session_refresh" "sh"
local out=$(run_vpn_switch session refresh 2>&1 || true)
if printf '%s\n' "$out" | grep -qE "No sessions found|No interfaces to reconcile|no changes|already consistent"; then
pass "Refresh on clean DB is a graceful no-op"
else
fail "Refresh on clean DB did not report a clean no-op"
printf '%s\n' "$out"
fi
}
# TEST 13: Session show - display connect script
test_session_show() {
test_header "session show - display connect script" "test_session_show"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations
setup_session_test_mocks
# Enable execution for wireguard add and import (narrow overrides)
set_function_interpreter "wireguard_add" "sh"
set_function_interpreter "wireguard_import1" "sh"
# Create session using public API (black-box testing)
run_vpn_switch wireguard add privacy >/dev/null 2>&1
local test_config="$TEST_BASE_DIR/wg-TEST-SHOW-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_show1
Address = 10.2.0.11/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_show1
AllowedIPs = 0.0.0.0/0
Endpoint = 11.11.11.11:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
run_vpn_switch wireguard add privacy wg-TEST-SHOW-1.conf show1 >/dev/null 2>&1
# Start VPN and extract session ID from output (black-box - use what program tells us)
local start_output=$(run_vpn_switch wireguard start privacy 2>&1 || true)
# Extract session PID from the output (what the user interface reports)
# Look for patterns like "Session: 12345" or "session 12345" or "PID: 12345"
local session_id=$(echo "$start_output" | grep -oE '[Ss]ession:? [0-9]+|PID:? [0-9]+|session [0-9]+' | grep -oE '[0-9]+' | head -1)
if [ -z "$session_id" ]; then
fail "Could not extract session ID from wireguard start output"
echo "Start output: $start_output"
return
fi
pass "Extracted session ID ($session_id) from start command output"
# Run session show with the PID we got from the program
local output=$(run_vpn_switch session show "$session_id" 2>&1 || true)
# Verify session metadata displayed
if printf '%s\n' "$output" | grep -q "Session:.*$session_id"; then
pass "Session ID displayed in header"
else
fail "Session ID not displayed"
printf '%s\n' "Output: $output"
fi
# Verify protocol shown
if printf '%s\n' "$output" | grep -q "Protocol:.*wireguard"; then
pass "Session protocol displayed"
else
fail "Session protocol not displayed"
printf '%s\n' "Output: $output"
fi
# Verify connect.sh contents displayed (should contain wg-quick commands)
if printf '%s\n' "$output" | grep -qiE "wg-quick|#!/bin/sh"; then
pass "Connect script contents displayed"
else
fail "Connect script contents not displayed"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 13b: Session show <name> — name resolution via API-driven setup
#
# Regression test: `vpn-switch session show <name>` is documented in
# TUTORIAL_SESSIONS.md (Step 7) but currently fails with "Session not found"
# because _session_show1 treats its argument as a literal PID and never
# consults the `session/` symlink directory. Compare to __session_start1 /
# __session_stop1, which both branch on numeric-vs-non-numeric and resolve
# the named symlink before dispatching.
#
# This test builds the precondition (a saved session named 'work') purely
# through the public API — `wireguard start` to bring a session into
# existence, then `session save work` to create the named link — and then
# calls `session show work` to assert that name resolution works. Until
# the bug is fixed, the assertion fails.
test_session_show_by_name() {
test_header "session show <name> - resolve named session symlink" "test_session_show_by_name"
test_setup
setup_session_test_mocks
# Enable execution for the API calls we'll drive
set_function_interpreter "wireguard_add" "sh"
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "session_save" "sh"
# Build state via the public API (no hand-crafted .session/<pid>/)
run_vpn_switch wireguard add privacy >/dev/null 2>&1
local test_config="$TEST_BASE_DIR/wg-TEST-SHOWNAME-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_showname1
Address = 10.2.0.21/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_showname1
AllowedIPs = 0.0.0.0/0
Endpoint = 21.21.21.21:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
run_vpn_switch wireguard add privacy wg-TEST-SHOWNAME-1.conf showname1 >/dev/null 2>&1
# Start a session and capture its PID from the output (black-box)
local start_output=$(run_vpn_switch wireguard start privacy 2>&1 || true)
local session_id=$(echo "$start_output" | grep -oE '[Ss]ession:? [0-9]+|PID:? [0-9]+|session [0-9]+' | grep -oE '[0-9]+' | head -1)
if [ -z "$session_id" ]; then
fail "Could not extract session ID from wireguard start output"
echo "Start output: $start_output"
rm -f "$test_config"
return
fi
pass "Started session $session_id via API"
# Save the session as 'work' via the public API (no manual ln -sfn)
run_vpn_switch session save work >/dev/null 2>&1
local save_output=$(run_vpn_switch session save work 2>&1 || true)
if printf '%s\n' "$save_output" | grep -qiE "saved.*work|work"; then
pass "Saved session $session_id as 'work' via API"
else
fail "Could not save session as 'work'"
printf '%s\n' "Save output: $save_output"
rm -f "$test_config"
return
fi
# Control case: `session show <PID>` works (no name resolution needed)
local by_pid=$(run_vpn_switch session show "$session_id" 2>&1 || true)
if printf '%s\n' "$by_pid" | grep -q "Session:.*$session_id"; then
pass "session show <PID> works (control case)"
else
fail "session show <PID> broken — control case failed, cannot assess name-resolution bug"
printf '%s\n' "Output: $by_pid"
rm -f "$test_config"
return
fi
# The bug under test: `session show <name>` should resolve the symlink
local by_name=$(run_vpn_switch session show work 2>&1 || true)
if printf '%s\n' "$by_name" | grep -q "Session:.*$session_id"; then
pass "session show <name> resolves named symlink"
else
fail "BUG: session show <name> does not resolve named symlink (mirrors session start/stop)"
printf '%s\n' "Output: $by_name"
fi
rm -f "$test_config"
}
# TEST 14: Session remove - remove stale session and refuse active
test_session_remove() {
test_header "session remove - remove stale, refuse active" "test_session_remove"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations
setup_session_test_mocks
# Enable execution for session remove, wireguard add and import (narrow overrides)
set_function_interpreter "session_remove" "sh"
set_function_interpreter "wireguard_add" "sh"
set_function_interpreter "wireguard_import1" "sh"
# Manually create ONLY the stale session (dead process) for testing stale removal
# This is what we're testing: can we remove dead sessions?
local dead_pid=99996
local session_dir="$TEST_DIR/.session/$dead_pid"
mkdir -p "$session_dir"
echo "$dead_pid" > "$session_dir/pid"
echo "wireguard" > "$session_dir/protocol"
echo "$TEST_DIR/wireguard/test-config.conf" > "$session_dir/original"
echo "wg_test2" > "$session_dir/interface"
date "+%Y-%m-%d %H:%M:%S" > "$session_dir/started"
echo "99995" > "$session_dir/vpn.pid" # Dead VPN process PID (stale session)
# Verify stale session exists
if [ -d "$session_dir" ]; then
pass "Stale session created for removal test"
else
fail "Stale session not created"
fi
# Run session remove on stale session
local output=$(run_vpn_switch session remove "$dead_pid" 2>&1 || true)
# Verify removal commands generated or session removed
if printf '%s\n' "$output" | grep -qE "Removing session.*$dead_pid|rm.*$dead_pid|removed"; then
pass "Removal operation logged for stale session"
else
fail "Removal not logged"
printf '%s\n' "Output: $output"
fi
# Now test refusal to remove active session
# We need to manually create an "active" session (one with a running process PID)
# This documents what makes a session "active": having a PID that corresponds to a running process
# In a mocked environment (where wg-quick is echo'd), using wireguard start would create a
# session without a real VPN process, so it would be detected as stale
local active_pid=$$ # Use current shell PID (definitely running)
local active_session="$TEST_DIR/.session/$active_pid"
mkdir -p "$active_session"
echo "$active_pid" > "$active_session/pid"
echo "wireguard" > "$active_session/protocol"
echo "$TEST_DIR/wireguard/test-config.conf" > "$active_session/original"
echo "wg_test4" > "$active_session/interface"
date "+%Y-%m-%d %H:%M:%S" > "$active_session/started"
echo "$$" > "$active_session/vpn.pid" # VPN process PID (current shell - definitely running)
if [ -d "$active_session" ]; then
pass "Active session (PID $$) created for refusal test"
else
fail "Active session not created"
return
fi
# Try to remove active session (should refuse)
local error_output=$(run_vpn_switch session remove "$active_pid" 2>&1 || true)
if echo "$error_output" | grep -qiE "error.*active|refusing.*active|still running"; then
pass "Refuses to remove active session (PID $active_pid)"
else
fail "Should refuse to remove active session"
echo "Output: $error_output"
fi
}
# TEST 15: Session save - save current session as default
test_session_save_default() {
test_header "session save - save current session as default" "test_session_save_default"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations
setup_session_test_mocks
# Enable execution for session_save (narrow override)
set_function_interpreter "session_save" "sh"
# Manually create active session (with running process PID)
# We must manually create this because in a mocked environment, `wireguard start`
# creates a session without a real VPN process, so it appears "stale"
# For session save to work, we need an "active" session (PID of running process)
local test_pid=$$ # Current shell PID (definitely running)
local session_dir="$TEST_DIR/.session/$test_pid"
mkdir -p "$session_dir"
echo "$test_pid" > "$session_dir/pid"
echo "wireguard" > "$session_dir/protocol"
echo "$TEST_DIR/wireguard/test-config.conf" > "$session_dir/original"
echo "wg9" > "$session_dir/interface"
date "+%Y-%m-%d %H:%M:%S" > "$session_dir/started"
echo "#!/bin/sh" > "$session_dir/connect.sh"
chmod 0755 "$session_dir/connect.sh"
# Create session/latest symlink (required by _session_save0 validation)
mkdir -p "$TEST_DIR/session"
ln -sfn "../.session/$test_pid" "$TEST_DIR/session/latest"
if [ -d "$session_dir" ] && [ -L "$TEST_DIR/session/latest" ]; then
pass "Active session (PID $$) with latest symlink created for save test"
else
fail "Active session or latest symlink not created"
return
fi
# Run session save (should create default symlink)
local output=$(run_vpn_switch session save 2>&1 || true)
# Verify output mentions saving or symlink creation
if printf '%s\n' "$output" | grep -qiE "saving|session.*saved|ln.*default"; then
pass "Session save operation logged"
else
fail "Session save not logged"
printf '%s\n' "Output: $output"
fi
# Verify default symlink was created
if [ -L "$TEST_DIR/session/default" ]; then
pass "Default session symlink created"
else
fail "Default session symlink not created"
ls -la "$TEST_DIR/session/" 2>/dev/null || echo "session/ directory not found"
fi
# Verify symlink points to the correct session
if [ -L "$TEST_DIR/session/default" ]; then
local link_target=$(readlink "$TEST_DIR/session/default")
if echo "$link_target" | grep -q "$test_pid"; then
pass "Default symlink points to session $test_pid"
else
fail "Default symlink target incorrect: $link_target"
fi
fi
}
# TEST 16: Session save - save with custom name
test_session_save_named() {
test_header "session save work - save current session with name" "test_session_save_named"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations
setup_session_test_mocks
# Enable execution for session_save (narrow override)
set_function_interpreter "session_save" "sh"
# Manually create active session (with running process PID)
local test_pid=$$ # Current shell PID (definitely running)
local session_dir="$TEST_DIR/.session/$test_pid"
mkdir -p "$session_dir"
echo "$test_pid" > "$session_dir/pid"
echo "wireguard" > "$session_dir/protocol"
echo "$TEST_DIR/wireguard/test-config.conf" > "$session_dir/original"
echo "wg9" > "$session_dir/interface"
date "+%Y-%m-%d %H:%M:%S" > "$session_dir/started"
echo "#!/bin/sh" > "$session_dir/connect.sh"
chmod 0755 "$session_dir/connect.sh"
# Create session/latest symlink (required by _session_save1 validation)
mkdir -p "$TEST_DIR/session"
ln -sfn "../.session/$test_pid" "$TEST_DIR/session/latest"
if [ -d "$session_dir" ] && [ -L "$TEST_DIR/session/latest" ]; then
pass "Active session (PID $$) with latest symlink created for named save test"
else
fail "Active session or latest symlink not created"
return
fi
# Run session save with name "work"
local output=$(run_vpn_switch session save work 2>&1 || true)
# Verify output mentions saving or symlink creation
if printf '%s\n' "$output" | grep -qiE "saving.*work|session.*saved.*work|ln.*work"; then
pass "Named session save operation logged"
else
fail "Named session save not logged"
printf '%s\n' "Output: $output"
fi
# Verify named symlink was created
if [ -L "$TEST_DIR/session/work" ]; then
pass "Named session symlink 'work' created"
else
fail "Named session symlink not created"
ls -la "$TEST_DIR/session/" 2>/dev/null || echo "session/ directory not found"
fi
# Verify symlink points to the correct session
if [ -L "$TEST_DIR/session/work" ]; then
local link_target=$(readlink "$TEST_DIR/session/work")
if echo "$link_target" | grep -q "$test_pid"; then
pass "Named symlink points to session $test_pid"
else
fail "Named symlink target incorrect: $link_target"
fi
fi
}
# TEST 17: Session start - resume default session
test_session_start_default() {
test_header "session start - resume default session" "test_session_start_default"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations
setup_session_test_mocks
# Manually create a mock session for resumption testing
# We're testing the resume functionality, so we need a pre-existing session
local test_pid=12345
local session_dir="$TEST_DIR/.session/$test_pid"
mkdir -p "$session_dir"
echo "$test_pid" > "$session_dir/pid"
echo "wireguard" > "$session_dir/protocol"
echo "$TEST_DIR/wireguard/test-config.conf" > "$session_dir/original"
echo "wg9" > "$session_dir/interface"
date "+%Y-%m-%d %H:%M:%S" > "$session_dir/started"
# Create connect.sh script with recognizable content
cat > "$session_dir/connect.sh" <<'EOF'
#!/bin/sh
# Test connect script for session 12345
echo "Resuming default session"
EOF
chmod 0755 "$session_dir/connect.sh"
# Create session/ directory and default symlink
mkdir -p "$TEST_DIR/session"
ln -sf "../.session/$test_pid" "$TEST_DIR/session/default"
if [ -L "$TEST_DIR/session/default" ]; then
pass "Default session symlink created for resume test"
else
fail "Default session symlink not created"
return
fi
# Run session start (should resolve default symlink and resume)
local output=$(run_vpn_switch session start 2>&1 || true)
# Verify it outputs the session connect command or references the PID
if printf '%s\n' "$output" | grep -qE "session.*start.*$test_pid|Resuming|connect"; then
pass "Session start resolves default symlink (PID $test_pid)"
else
fail "Session start did not resolve default"
printf '%s\n' "Output: $output"
fi
}
# TEST 18: Session start - resume named session
test_session_start_named() {
test_header "session start work - resume named session" "test_session_start_named"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations
setup_session_test_mocks
# Manually create a mock session for resumption testing
local test_pid=67890
local session_dir="$TEST_DIR/.session/$test_pid"
mkdir -p "$session_dir"
echo "$test_pid" > "$session_dir/pid"
echo "wireguard" > "$session_dir/protocol"
echo "$TEST_DIR/wireguard/test-config.conf" > "$session_dir/original"
echo "wg9" > "$session_dir/interface"
date "+%Y-%m-%d %H:%M:%S" > "$session_dir/started"
# Create connect.sh script with recognizable content
cat > "$session_dir/connect.sh" <<'EOF'
#!/bin/sh
# Test connect script for work session
echo "Resuming work session"
EOF
chmod 0755 "$session_dir/connect.sh"
# Create session/ directory and named symlink "work"
mkdir -p "$TEST_DIR/session"
ln -sf "../.session/$test_pid" "$TEST_DIR/session/work"
if [ -L "$TEST_DIR/session/work" ]; then
pass "Named session symlink 'work' created for resume test"
else
fail "Named session symlink not created"
return
fi
# Run session start with name "work"
local output=$(run_vpn_switch session start work 2>&1 || true)
# Verify it outputs the session connect command or references the PID
if printf '%s\n' "$output" | grep -qE "session.*start.*$test_pid|work.*session|Resuming work|connect"; then
pass "Session start resolves named session 'work' (PID $test_pid)"
else
fail "Session start did not resolve named session"
printf '%s\n' "Output: $output"
fi
}
# TEST 19: Session start - resume by PID
test_session_start_bypid() {
test_header "session start <PID> - resume session by PID" "test_session_start_bypid"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations
setup_session_test_mocks
# Manually create a mock session for resumption testing
local test_pid=11111
local session_dir="$TEST_DIR/.session/$test_pid"
mkdir -p "$session_dir"
echo "$test_pid" > "$session_dir/pid"
echo "wireguard" > "$session_dir/protocol"
echo "$TEST_DIR/wireguard/test-config.conf" > "$session_dir/original"
echo "wg9" > "$session_dir/interface"
date "+%Y-%m-%d %H:%M:%S" > "$session_dir/started"
# Create connect.sh script with recognizable content
cat > "$session_dir/connect.sh" <<'EOF'
#!/bin/sh
# Test connect script for PID session
echo "Resuming PID session"
EOF
chmod 0755 "$session_dir/connect.sh"
if [ -d "$session_dir" ]; then
pass "Mock session (PID $test_pid) created for PID resume test"
else
fail "Mock session not created"
return
fi
# Run session start with explicit PID
local output=$(run_vpn_switch session start "$test_pid" 2>&1 || true)
# Verify it outputs the session connect command or references the PID
if printf '%s\n' "$output" | grep -qE "session.*start.*$test_pid|Resuming.*PID|connect"; then
pass "Session start resumes by explicit PID ($test_pid)"
else
fail "Session start did not resume by PID"
printf '%s\n' "Output: $output"
fi
}
# TEST 20: Session stop - name routes to disconnect via PID resolution
#
# Tests __session_stop1's routing only:
# name → resolve_session_id → PID → session disconnect <pid>
#
# Aliveness intentionally does NOT enter this path; disconnect is
# idempotent and handles dead sessions gracefully. See session.sh
# (___session_stop0 / __session_stop1) for the architectural decision.
test_session_stop_named() {
test_header "session stop work - resolves name to disconnect" "test_session_stop_named"
test_setup
setup_session_test_mocks
# Inspect __session_stop1's combinator output without executing it
override_for_command_inspection "session_stop1"
# Minimal session fixture: just a session_dir and a named symlink.
# No vpn.pid, no interface, no ownership — stop1 doesn't read them.
local test_pid=77777
local session_dir="$TEST_DIR/.session/$test_pid"
mkdir -p "$session_dir"
mkdir -p "$TEST_DIR/session"
ln -sf "../.session/$test_pid" "$TEST_DIR/session/work"
local output=$(run_vpn_switch session stop work 2>&1 || true)
if printf '%s\n' "$output" | grep -qE "session disconnect \"?${test_pid}\"?"; then
pass "session stop 'work' emits 'session disconnect $test_pid'"
else
fail "session stop did not route name to disconnect"
printf '%s\n' "Output: $output"
fi
}
# TEST 21: Session stop - explicit PID routes directly to disconnect
test_session_stop_bypid() {
test_header "session stop <PID> - routes PID to disconnect" "test_session_stop_bypid"
test_setup
setup_session_test_mocks
override_for_command_inspection "session_stop1"
# Minimal session fixture, no metadata needed for stop1's routing.
local test_pid=88888
local session_dir="$TEST_DIR/.session/$test_pid"
mkdir -p "$session_dir"
local output=$(run_vpn_switch session stop "$test_pid" 2>&1 || true)
if printf '%s\n' "$output" | grep -qE "session disconnect \"?${test_pid}\"?"; then
pass "session stop $test_pid emits 'session disconnect $test_pid'"
else
fail "session stop did not route PID to disconnect"
printf '%s\n' "Output: $output"
fi
}
# TEST 22: Session stop - inactive session also routes to disconnect (idempotency contract)
#
# Documents the architectural choice that 'stop' does not branch on aliveness:
# inactive sessions get the same 'session disconnect' routing, and the
# disconnect chain is idempotent (kill on dead PID is no-op, wg-quick down on
# missing interface is no-op, firewall revert is no-op).
test_session_stop_inactive() {
test_header "session stop <PID> - inactive session routes to disconnect (idempotent)" "test_session_stop_inactive"
test_setup
setup_session_test_mocks
override_for_command_inspection "session_stop1"
# Inactive fixture: session dir exists, no vpn.pid. stop1 doesn't care.
local test_pid=99999
local session_dir="$TEST_DIR/.session/$test_pid"
mkdir -p "$session_dir"
local output=$(run_vpn_switch session stop "$test_pid" 2>&1 || true)
if printf '%s\n' "$output" | grep -qE "session disconnect \"?${test_pid}\"?"; then
pass "Inactive session still routes to disconnect (no 'not active' branch)"
else
fail "Inactive session was not routed to disconnect"
printf '%s\n' "Output: $output"
fi
}
# TEST 22a: ___session_stop0 batch - emits one 'session stop <pid>' per session
#
# Verifies the new batch combinator iterates all sessions instead of
# picking one (avoids leaving an active session running when "latest"
# happens to be dead).
test_session_stop_batch_all() {
test_header "session stop (no arg) - batch emits stop per session" "test_session_stop_batch_all"
test_setup
setup_session_test_mocks
# Inspect ___session_stop0's batch output without executing it
override_for_command_inspection "session_stop0"
# Two minimal sessions
mkdir -p "$TEST_DIR/.session/11111" "$TEST_DIR/.session/22222"
local output=$(run_vpn_switch session stop 2>&1 || true)
if printf '%s\n' "$output" | grep -qE 'session stop "?11111"?'; then
pass "batch emits 'session stop 11111'"
else
fail "batch missing 'session stop 11111'"
printf '%s\n' "Output: $output"
fi
if printf '%s\n' "$output" | grep -qE 'session stop "?22222"?'; then
pass "batch emits 'session stop 22222'"
else
fail "batch missing 'session stop 22222'"
fi
}
# TEST 23: WireGuard remove - refuse to remove config in use by session
test_wireguard_remove_config_in_use() {
test_header "wireguard remove - refuse removal when config in use" "test_wireguard_remove_config_in_use"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard import and add (narrow overrides)
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "wireguard_add" "sh"
# Import a test config that we'll mark as "in use"
local test_config="$TEST_BASE_DIR/wg-TEST-RM-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_rm1
Address = 10.2.0.20/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_rm1
AllowedIPs = 0.0.0.0/0
Endpoint = 20.20.20.20:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Manually create active session using this config
# This documents the safety check: cannot remove configs in use by active sessions
local real_pid=$$ # Current shell PID (definitely running)
local session_dir="$TEST_DIR/.session/$real_pid"
mkdir -p "$session_dir"
echo "$real_pid" > "$session_dir/pid"
echo "wireguard" > "$session_dir/protocol"
echo "$TEST_DIR/wireguard/wg-TEST-RM-1.conf" > "$session_dir/original"
echo "wg_test_safe" > "$session_dir/interface"
date "+%Y-%m-%d %H:%M:%S" > "$session_dir/started"
if [ -d "$session_dir" ]; then
pass "Active session (PID $$) created using config"
else
fail "Active session not created"
return
fi
# Try to remove the config that's in use
local output=$(run_vpn_switch wireguard remove wg-TEST-RM-1.conf 2>&1 || true)
# Verify error message about config in use
if printf '%s\n' "$output" | grep -qiE "error.*in use|config in use|session.*using"; then
pass "Error message when config in use by session"
else
fail "Missing error for config in use"
printf '%s\n' "Output: $output"
fi
# Verify error mentions session removal or PID
if printf '%s\n' "$output" | grep -qE "session remove|PID.*$real_pid"; then
pass "Error suggests removing session first"
else
fail "Error doesn't suggest session removal"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 21: WireGuard remove - refuse to remove config with symlink references
test_wireguard_remove_config_with_refs() {
test_header "wireguard remove - refuse removal when config has symlink refs" "test_wireguard_remove_config_with_refs"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard import, add, link (narrow overrides)
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "wireguard_add" "sh"
set_function_interpreter "wireguard_link2" "sh"
# Import a test config
local test_config="$TEST_BASE_DIR/wg-TEST-RM-2.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_rm2
Address = 10.2.0.21/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_rm2
AllowedIPs = 0.0.0.0/0
Endpoint = 21.21.21.21:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Create privacy category and link the config to it
run_vpn_switch wireguard add privacy >/dev/null 2>&1
run_vpn_switch wireguard add privacy wg-TEST-RM-2.conf rm2 >/dev/null 2>&1
# Verify symlink was created
if [ -L "$TEST_DIR/wireguard/privacy/rm2" ]; then
pass "Category symlink created (privacy/rm2)"
else
fail "Category symlink not created"
return
fi
# Try to remove the config that has symlink references
local output=$(run_vpn_switch wireguard remove wg-TEST-RM-2.conf 2>&1 || true)
# Verify error message about references
if printf '%s\n' "$output" | grep -qiE "error.*reference|has.*reference|symlink.*pointing"; then
pass "Error message when config has symlink references"
else
fail "Missing error for config with references"
printf '%s\n' "Output: $output"
fi
# Verify error lists the references or category
if printf '%s\n' "$output" | grep -qE "privacy/rm2|privacy"; then
pass "Error lists symlink references"
else
fail "Error doesn't list references"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 22: WireGuard remove - remove symlink without safety checks
test_wireguard_remove_symlink() {
test_header "wireguard remove - remove symlink (no safety checks)" "test_wireguard_remove_symlink"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard import, remove (narrow overrides)
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "wireguard_remove" "sh"
# Import a config first
local test_config="$TEST_BASE_DIR/wg-TEST-RM-3.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_rm3
Address = 10.2.0.22/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_rm3
AllowedIPs = 0.0.0.0/0
Endpoint = 22.22.22.22:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Create a test symlink in protocol directory
ln -sf "./wg-TEST-RM-3.conf" "$TEST_DIR/wireguard/test-remove-link"
# Verify link was created
if [ -L "$TEST_DIR/wireguard/test-remove-link" ]; then
pass "Test symlink created"
else
fail "Failed to create test symlink"
rm -f "$test_config"
return
fi
# Run remove command on symlink (should succeed without safety checks)
local output=$(run_vpn_switch wireguard remove test-remove-link 2>&1 || true)
# Verify removal operation logged
if printf '%s\n' "$output" | grep -qE "rm.*test-remove-link|Remov.*test-remove-link"; then
pass "Symlink removal operation logged"
else
fail "Removal not logged"
printf '%s\n' "Output: $output"
fi
# Verify symlink was actually removed (if execution enabled)
if [ ! -L "$TEST_DIR/wireguard/test-remove-link" ]; then
pass "Symlink was removed"
else
# Not a failure if just showing commands
pass "Remove command generated for symlink"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 23: WireGuard remove - remove link from category
test_wireguard_remove_category_link() {
test_header "wireguard remove - remove link from category" "test_wireguard_remove_category_link"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard import, add, remove (narrow overrides)
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "wireguard_add" "sh"
set_function_interpreter "wireguard_remove" "sh"
# Import a config
local test_config="$TEST_BASE_DIR/wg-TEST-RM-4.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_rm4
Address = 10.2.0.23/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_rm4
AllowedIPs = 0.0.0.0/0
Endpoint = 23.23.23.23:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Create privacy category and link the config
run_vpn_switch wireguard add privacy >/dev/null 2>&1
run_vpn_switch wireguard add privacy wg-TEST-RM-4.conf rm4 >/dev/null 2>&1
# Verify category link exists
if [ -L "$TEST_DIR/wireguard/privacy/rm4" ]; then
pass "Category link (privacy/rm4) created"
else
fail "Category link not created"
return
fi
# Run remove command on category link
local output=$(run_vpn_switch wireguard remove privacy rm4 2>&1 || true)
# Verify removal operation logged
if printf '%s\n' "$output" | grep -qE "rm.*privacy/rm4|Remov.*privacy.*rm4"; then
pass "Category link removal operation logged"
else
fail "Removal not logged"
printf '%s\n' "Output: $output"
fi
# Verify category link was removed (if execution enabled)
if [ ! -L "$TEST_DIR/wireguard/privacy/rm4" ]; then
pass "Category link was removed"
else
# Not a failure if just showing commands
pass "Remove command generated for category link"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 24: WireGuard clean - no broken links
test_wireguard_clean_no_broken() {
test_header "wireguard clean - no broken links found" "test_wireguard_clean_no_broken"
# Create exclusive database for this test
test_setup
# Override interpreter to inspect clean command output (profile-independent)
override_for_command_inspection "wireguard_clean"
# Enable execution for wireguard add (to ensure category exists)
set_function_interpreter "wireguard_add" "sh"
# Create privacy category to ensure there's something to scan
run_vpn_switch wireguard add privacy >/dev/null 2>&1
# Run clean on clean database (no broken links)
local output=$(run_vpn_switch wireguard clean 2>&1 || true)
# Verify header displayed
if printf '%s\n' "$output" | grep -qE "Clean.*broken.*symlink|wireguard/"; then
pass "Clean header displayed"
else
fail "Clean header missing"
printf '%s\n' "Output: $output"
fi
# Verify "no broken symlinks" message
if printf '%s\n' "$output" | grep -qiE "no broken symlinks|no.*broken.*found"; then
pass "No broken symlinks message displayed"
else
fail "No broken message missing"
printf '%s\n' "Output: $output"
fi
# Verify usage hint displayed (how to execute commands)
if printf '%s\n' "$output" | grep -qE "wireguard clean.*sh|pipe.*sh"; then
pass "Usage hint displayed"
else
fail "Usage hint missing"
printf '%s\n' "Output: $output"
fi
}
# TEST 25: WireGuard clean - broken protocol link
test_wireguard_clean_broken_protocol_link() {
test_header "wireguard clean - detect and generate remove for broken protocol link" "test_wireguard_clean_broken_protocol_link"
# Create exclusive database for this test
test_setup
# Override interpreter to inspect clean command output (profile-independent)
override_for_command_inspection "wireguard_clean"
# Create a broken symlink in protocol directory
# This simulates a config file that was deleted but symlinks remain
ln -sf "./nonexistent.conf" "$TEST_DIR/wireguard/broken-proto-link"
# Verify broken link was created
if [ -L "$TEST_DIR/wireguard/broken-proto-link" ] && [ ! -e "$TEST_DIR/wireguard/broken-proto-link" ]; then
pass "Broken protocol symlink created for testing"
else
fail "Failed to create broken symlink"
return
fi
# Run clean (should detect broken link and output remove command)
local output=$(run_vpn_switch wireguard clean 2>&1 || true)
# Verify remove command generated for broken link
if printf '%s\n' "$output" | grep -q "wireguard remove.*broken-proto-link"; then
pass "Remove command generated for broken protocol link"
else
fail "Remove command not generated"
printf '%s\n' "Output: $output"
fi
# Verify command is properly quoted (for safety with special characters)
if printf '%s\n' "$output" | grep -qE 'wireguard remove.*"broken-proto-link"|broken-proto-link'; then
pass "Command includes link name"
else
fail "Command formatting incorrect"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$TEST_DIR/wireguard/broken-proto-link"
}
# TEST 26: WireGuard clean - broken category link
test_wireguard_clean_broken_category_link() {
test_header "wireguard clean - detect and generate remove for broken category link" "test_wireguard_clean_broken_category_link"
# Create exclusive database for this test
test_setup
override_for_command_inspection "wireguard_clean"
# Enable execution for wireguard add (narrow override)
set_function_interpreter "wireguard_add" "sh"
# Create privacy category
run_vpn_switch wireguard add privacy >/dev/null 2>&1
# Create a broken symlink in category
ln -sf "../nonexistent.conf" "$TEST_DIR/wireguard/privacy/broken-cat-link"
# Verify broken link was created
if [ -L "$TEST_DIR/wireguard/privacy/broken-cat-link" ] && [ ! -e "$TEST_DIR/wireguard/privacy/broken-cat-link" ]; then
pass "Broken category symlink created for testing"
else
fail "Failed to create broken category symlink"
return
fi
# Run clean (should detect broken link in category)
local output=$(run_vpn_switch wireguard clean 2>&1 || true)
# Verify remove command generated with category and link name
if printf '%s\n' "$output" | grep -q "wireguard remove.*privacy.*broken-cat-link"; then
pass "Remove command generated for broken category link"
else
fail "Remove command not generated or incorrect"
printf '%s\n' "Output: $output"
fi
# Verify both category and link name are in command
if printf '%s\n' "$output" | grep "wireguard remove" | grep -q "privacy"; then
pass "Command includes category name"
else
fail "Category not in command"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$TEST_DIR/wireguard/privacy/broken-cat-link"
}
# TEST 27: WireGuard clean - category with no broken links
test_wireguard_clean_category_no_broken() {
test_header "wireguard clean privacy - category with no broken links" "test_wireguard_clean_category_no_broken"
# Create exclusive database for this test
test_setup
override_for_command_inspection "wireguard_clean"
# Enable execution for wireguard add (narrow override)
set_function_interpreter "wireguard_add" "sh"
# Create privacy category
run_vpn_switch wireguard add privacy >/dev/null 2>&1
# Run clean on specific category (no broken links)
local output=$(run_vpn_switch wireguard clean privacy 2>&1 || true)
# Verify category-specific header
if printf '%s\n' "$output" | grep -qE "wireguard/privacy/|Clean.*privacy"; then
pass "Category-specific header displayed"
else
fail "Category-specific header missing"
printf '%s\n' "Output: $output"
fi
# Verify "no broken symlinks" message for category
if printf '%s\n' "$output" | grep -qiE "no broken.*privacy|no.*broken.*found"; then
pass "No broken symlinks message for category"
else
fail "Category message missing"
printf '%s\n' "Output: $output"
fi
}
# TEST 28: WireGuard clean - category with broken links
test_wireguard_clean_category_with_broken() {
test_header "wireguard clean privacy - category with broken links" "test_wireguard_clean_category_with_broken"
# Create exclusive database for this test
test_setup
override_for_command_inspection "wireguard_clean"
# Enable execution for wireguard add (narrow override)
set_function_interpreter "wireguard_add" "sh"
# Create privacy category
run_vpn_switch wireguard add privacy >/dev/null 2>&1
# Create a broken symlink in privacy category
ln -sf "../nonexistent.conf" "$TEST_DIR/wireguard/privacy/broken-test-link"
# Verify broken link was created
if [ -L "$TEST_DIR/wireguard/privacy/broken-test-link" ] && [ ! -e "$TEST_DIR/wireguard/privacy/broken-test-link" ]; then
pass "Broken symlink created in privacy category"
else
fail "Failed to create broken symlink"
return
fi
# Run clean on specific category
local output=$(run_vpn_switch wireguard clean privacy 2>&1 || true)
# Verify remove command generated for specific category
if printf '%s\n' "$output" | grep -q "wireguard remove.*privacy.*broken-test-link"; then
pass "Remove command generated for category link"
else
fail "Remove command not generated"
printf '%s\n' "Output: $output"
fi
# Verify only this category is scanned (not entire wireguard directory)
if printf '%s\n' "$output" | grep -qE "wireguard/privacy/|Clean.*privacy"; then
pass "Category-specific clean (not global)"
else
fail "Should be category-specific clean"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$TEST_DIR/wireguard/privacy/broken-test-link"
}
# TEST 29: WireGuard validate - detect broken symlinks in protocol directory
test_wireguard_validate_broken_symlink() {
test_header "wireguard validate - detect broken symlinks" "test_wireguard_validate_broken_symlink"
# Create exclusive database for this test
test_setup
# Create a broken symlink in protocol directory
# This simulates a config that was deleted but symlinks remain
ln -sf "./nonexistent.conf" "$TEST_DIR/wireguard/broken-link"
# Verify broken link was created
if [ -L "$TEST_DIR/wireguard/broken-link" ] && [ ! -e "$TEST_DIR/wireguard/broken-link" ]; then
pass "Broken symlink created for validation test"
else
fail "Failed to create broken symlink"
return
fi
# Run validate (should detect broken symlink)
local output=$(run_vpn_switch wireguard validate 2>&1 || true)
# Verify broken symlink detected with ERROR marker
if printf '%s\n' "$output" | grep -qE "\[ERROR\].*Broken symlink.*broken-link|Broken.*wireguard/broken-link"; then
pass "Broken symlink detected in protocol directory"
else
fail "Broken symlink not detected"
printf '%s\n' "Output: $output"
fi
# Verify error status reported
if printf '%s\n' "$output" | grep -qiE "error.*found|issues found"; then
pass "Error status reported"
else
fail "Error status not reported"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$TEST_DIR/wireguard/broken-link"
}
# TEST 30: WireGuard validate - detect broken symlinks in categories
test_wireguard_validate_broken_category_symlink() {
test_header "wireguard validate - detect broken symlinks in categories" "test_wireguard_validate_broken_category_symlink"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard add (narrow override)
set_function_interpreter "wireguard_add" "sh"
# Create privacy category
run_vpn_switch wireguard add privacy >/dev/null 2>&1
# Create a broken symlink in category
ln -sf "../nonexistent.conf" "$TEST_DIR/wireguard/privacy/broken-cat-link"
# Verify broken link was created
if [ -L "$TEST_DIR/wireguard/privacy/broken-cat-link" ] && [ ! -e "$TEST_DIR/wireguard/privacy/broken-cat-link" ]; then
pass "Broken category symlink created for validation test"
else
fail "Failed to create broken category symlink"
return
fi
# Run validate (should detect broken symlink in category)
local output=$(run_vpn_switch wireguard validate 2>&1 || true)
# Verify broken symlink detected in category
if printf '%s\n' "$output" | grep -qE "\[ERROR\].*Broken symlink.*privacy/broken-cat-link|Broken.*wireguard/privacy/broken-cat-link"; then
pass "Broken symlink detected in category"
else
fail "Broken symlink not detected in category"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$TEST_DIR/wireguard/privacy/broken-cat-link"
}
# TEST 31: WireGuard validate - detect naming conflicts
test_wireguard_validate_naming_conflict() {
test_header "wireguard validate - detect naming conflicts" "test_wireguard_validate_naming_conflict"
# Create exclusive database for this test
test_setup
# Create a config file and directory with conflicting names
# This simulates: test-conflict.conf (file) and test-conflict/ (directory)
# which creates ambiguity in resolution
touch "$TEST_DIR/wireguard/test-conflict.conf"
mkdir -p "$TEST_DIR/wireguard/test-conflict"
# Verify both exist
if [ -f "$TEST_DIR/wireguard/test-conflict.conf" ] && [ -d "$TEST_DIR/wireguard/test-conflict" ]; then
pass "Naming conflict created (file + directory)"
else
fail "Failed to create naming conflict"
return
fi
# Run validate (should detect naming conflict)
local output=$(run_vpn_switch wireguard validate 2>&1 || true)
# Verify naming conflict detected with WARNING marker
if printf '%s\n' "$output" | grep -qiE "\[WARNING\].*naming conflict.*test-conflict|conflict.*test-conflict"; then
pass "Naming conflict detected"
else
fail "Naming conflict not detected"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$TEST_DIR/wireguard/test-conflict.conf"
rmdir "$TEST_DIR/wireguard/test-conflict" 2>/dev/null || true
}
# TEST 32: WireGuard validate - detect invalid config files
test_wireguard_validate_invalid_config() {
test_header "wireguard validate - detect invalid config files" "test_wireguard_validate_invalid_config"
# Create exclusive database for this test
test_setup
# Create an empty config file (invalid - no content)
touch "$TEST_DIR/wireguard/empty-test.conf"
# Verify empty file exists
if [ -f "$TEST_DIR/wireguard/empty-test.conf" ]; then
pass "Empty config file created for validation test"
else
fail "Failed to create empty config"
return
fi
# Run validate (should detect empty config)
local output=$(run_vpn_switch wireguard validate 2>&1 || true)
# Verify empty file detected with WARNING marker
if printf '%s\n' "$output" | grep -qiE "\[WARNING\].*empty.*empty-test.conf|empty.*file"; then
pass "Empty config file detected"
else
fail "Empty config file not detected"
printf '%s\n' "Output: $output"
fi
# Create config without [Interface] section (invalid structure)
cat > "$TEST_DIR/wireguard/no-interface.conf" <<'EOF'
# Just a comment, no actual config
# Missing [Interface] section
EOF
# Run validate again (should detect missing Interface section)
local output2=$(run_vpn_switch wireguard validate 2>&1 || true)
# Verify missing Interface section detected
if echo "$output2" | grep -qiE "\[WARNING\].*interface.*no-interface.conf|missing.*interface"; then
pass "Missing [Interface] section detected"
else
fail "Missing [Interface] section not detected"
echo "Output: $output2"
fi
# Cleanup
rm -f "$TEST_DIR/wireguard/empty-test.conf"
rm -f "$TEST_DIR/wireguard/no-interface.conf"
}
# TEST 33: Stop - no sessions exist (empty output)
test_stop_no_sessions() {
test_header "stop - no sessions exist (empty output)" "test_stop_no_sessions"
# Create exclusive database for this test
test_setup
# Run stop when no sessions exist
local output=$(run_vpn_switch stop 2>&1 || true)
# When no sessions exist, combinator produces empty output or "already removed" message
# This is expected behavior - silence means "nothing to do"
if [ -z "$output" ] || printf '%s\n' "$output" | grep -qiE "already removed|no.*session"; then
pass "No output or log message when no sessions exist"
else
# Not a failure - may just show informational messages
pass "Stop command handled no sessions gracefully"
fi
}
# TEST 34: Stop - stop specific interface by name
test_stop_specific_interface() {
test_header "stop wg_test_safe - stop specific interface by name" "test_stop_specific_interface"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations
setup_session_test_mocks
# Manually create active session with wg_test_safe interface
local real_pid=$$ # Current shell PID (definitely running)
local session_dir="$TEST_DIR/.session/$real_pid"
mkdir -p "$session_dir"
echo "$real_pid" > "$session_dir/pid"
echo "wireguard" > "$session_dir/protocol"
echo "$TEST_DIR/wireguard/test-config.conf" > "$session_dir/original"
echo "wg_test_safe" > "$session_dir/interface"
date "+%Y-%m-%d %H:%M:%S" > "$session_dir/started"
if [ -d "$session_dir" ]; then
pass "Active session (PID $$, interface wg_test_safe) created"
else
fail "Active session not created"
return
fi
# Call stop with specific interface
local output=$(run_vpn_switch stop wg_test_safe 2>&1 || true)
# Verify interface-specific disconnect
if printf '%s\n' "$output" | grep -qE "Disconnect.*wg_test_safe|wg_test_safe"; then
pass "Stopped specific interface wg_test_safe"
else
fail "Did not stop specific interface"
printf '%s\n' "Output: $output"
fi
# Verify wg-quick down command for specific interface
if printf '%s\n' "$output" | grep -q "wg-quick down.*wg_test_safe"; then
pass "Interface wg_test_safe disconnect command generated"
else
fail "Interface disconnect command missing"
printf '%s\n' "Output: $output"
fi
}
# TEST 35: WireGuard disconnect - direct disconnect command
test_wireguard_disconnect_interface() {
test_header "wireguard disconnect wg_test_safe - direct disconnect command" "test_wireguard_disconnect_interface"
# Create exclusive database for this test
test_setup
# Override interpreter to inspect disconnect command output
override_for_command_inspection "wireguard_disconnect1"
# Set up mocks for network operations
setup_session_test_mocks
# Manually create active session
local real_pid=$$ # Current shell PID (definitely running)
local session_dir="$TEST_DIR/.session/$real_pid"
mkdir -p "$session_dir"
echo "$real_pid" > "$session_dir/pid"
echo "wireguard" > "$session_dir/protocol"
echo "$TEST_DIR/wireguard/test-config.conf" > "$session_dir/original"
echo "wg_test_safe" > "$session_dir/interface"
date "+%Y-%m-%d %H:%M:%S" > "$session_dir/started"
if [ -d "$session_dir" ]; then
pass "Active session (PID $$) created for disconnect test"
else
fail "Active session not created"
return
fi
# Call disconnect directly (protocol-specific command)
local output=$(run_vpn_switch wireguard disconnect wg_test_safe 2>&1 || true)
# Verify disconnect command executed
if printf '%s\n' "$output" | grep -qiE "disconnect.*wg_test_safe|Stopping.*wg_test_safe"; then
pass "Disconnect command executed for wg_test_safe"
else
fail "Disconnect command not executed"
printf '%s\n' "Output: $output"
fi
# Verify session PID displayed
if printf '%s\n' "$output" | grep -qE "Session.*PID.*$real_pid|PID.*$real_pid"; then
pass "Session PID displayed correctly"
else
# Not a failure - may not always show PID
pass "Disconnect command completed"
fi
}
# TEST 36: Stop - error when interface not found
test_stop_nonexistent_interface() {
test_header "stop wg_test_other - error when interface not found" "test_stop_nonexistent_interface"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations
setup_session_test_mocks
# Create session for wg_test_safe only
local real_pid=$$
local session_dir="$TEST_DIR/.session/$real_pid"
mkdir -p "$session_dir"
echo "$real_pid" > "$session_dir/pid"
echo "wireguard" > "$session_dir/protocol"
echo "$TEST_DIR/wireguard/test-config.conf" > "$session_dir/original"
echo "wg_test_safe" > "$session_dir/interface"
date "+%Y-%m-%d %H:%M:%S" > "$session_dir/started"
if [ -d "$session_dir" ]; then
pass "Active session with wg_test_safe interface created"
else
fail "Active session not created"
return
fi
# Try to stop wg_test_other (doesn't exist)
local output=$(run_vpn_switch stop wg_test_other 2>&1 || true)
# Verify error message for nonexistent interface
if printf '%s\n' "$output" | grep -qiE "error.*no.*session.*found|no.*session.*wg_test_other|no active.*session"; then
pass "Error message for nonexistent interface"
else
fail "Missing error for nonexistent interface"
printf '%s\n' "Output: $output"
fi
}
# TEST 37: Disconnect - error without session
test_disconnect_without_session() {
test_header "disconnect without session - error handling" "test_disconnect_without_session"
# Create exclusive database for this test
test_setup
# Ensure no sessions exist
rm -rf "$TEST_DIR/.session"/* 2>/dev/null || true
# Try to disconnect wg_test_disconnect when no session exists
# Use test-specific interface name to avoid touching production interfaces (wg0, wg1, etc.)
local output=$(run_vpn_switch wireguard disconnect wg_test_disconnect 2>&1 || true)
# Verify error message when no session exists
if printf '%s\n' "$output" | grep -qiE "error.*no.*session|no.*session found|session not found"; then
pass "Error message when no session exists"
else
fail "Missing error for disconnect without session"
printf '%s\n' "Output: $output"
fi
}
# TEST 38: Stop - idempotent behavior (stop twice)
test_stop_twice_idempotent() {
test_header "stop twice - idempotent (safe to call multiple times)" "test_stop_twice_idempotent"
# Create exclusive database for this test
test_setup
# Ensure no sessions exist
rm -rf "$TEST_DIR/.session"/* 2>/dev/null || true
# First stop (no sessions)
local output1=$(run_vpn_switch stop 2>&1 || true)
# Should handle gracefully (empty output or log message)
if [ -z "$output1" ] || echo "$output1" | grep -qiE "already removed|no.*session"; then
pass "First stop: handles no sessions gracefully"
else
# Not a failure - may show informational messages
pass "First stop: executed without error"
fi
# Second stop (still no sessions) - should be idempotent
local output2=$(run_vpn_switch stop 2>&1 || true)
# Should still handle gracefully (idempotent)
if [ -z "$output2" ] || echo "$output2" | grep -qiE "already removed|no.*session"; then
pass "Second stop: still handles no sessions (idempotent)"
else
# Not a failure - idempotent behavior confirmed by no error
pass "Second stop: executed without error (idempotent)"
fi
}
# TEST 39: WireGuard import - error when file doesn't exist
test_wireguard_import_nonexistent() {
test_header "wireguard import - error when file doesn't exist" "test_wireguard_import_nonexistent"
# Create exclusive database for this test
test_setup
# Try to import nonexistent file
local output=$(run_vpn_switch wireguard import /nonexistent/wg-XX-99.conf 2>&1 || true)
# Verify error message for nonexistent file
if printf '%s\n' "$output" | grep -qiE "error.*not found|does not exist|no such file"; then
pass "Error message for non-existent file"
else
fail "Missing error for non-existent file"
printf '%s\n' "Output: $output"
fi
}
# TEST 40: WireGuard import - error when config already exists
test_wireguard_import_duplicate() {
test_header "wireguard import - error when config already exists" "test_wireguard_import_duplicate"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard import (narrow override)
set_function_interpreter "wireguard_import1" "sh"
# First, import a config
local test_config="$TEST_BASE_DIR/wg-TEST-DUP-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_dup1
Address = 10.2.0.30/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_dup1
AllowedIPs = 0.0.0.0/0
Endpoint = 30.30.30.30:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Verify it was imported
if [ -f "$TEST_DIR/wireguard/wg-TEST-DUP-1.conf" ]; then
pass "First import succeeded"
else
fail "First import failed"
return
fi
# Try to import again (duplicate)
local output=$(run_vpn_switch wireguard import "$test_config" 2>&1 || true)
# Verify error message for duplicate
if printf '%s\n' "$output" | grep -qiE "error.*already exists|config.*exists"; then
pass "Error message when config already exists"
else
fail "Missing error for duplicate config"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 41: WireGuard add - error when category already exists
test_wireguard_add_category_conflict() {
test_header "wireguard add - error when category already exists" "test_wireguard_add_category_conflict"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard add (narrow override)
set_function_interpreter "wireguard_add" "sh"
# First, create privacy category
run_vpn_switch wireguard add privacy >/dev/null 2>&1
# Verify it was created
if [ -d "$TEST_DIR/wireguard/privacy" ]; then
pass "First add succeeded (privacy category created)"
else
fail "First add failed"
return
fi
# Try to create it again (conflict)
local output=$(run_vpn_switch wireguard add privacy 2>&1 || true)
# Verify error message for existing category
if printf '%s\n' "$output" | grep -qiE "error.*already exists|category.*exists|directory.*exists"; then
pass "Error message when category already exists"
else
fail "Missing error for existing category"
printf '%s\n' "Output: $output"
fi
}
# TEST 42: WireGuard add - arity-2 delegates to add3
test_wireguard_add_link_arity2() {
test_header "wireguard add - arity-2 delegates to add3 with target as alias" "test_wireguard_add_category_conflict"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard import and add (narrow overrides)
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "wireguard_add" "sh"
# Import a config first
local test_config="$TEST_BASE_DIR/wg-TEST-AR2-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_ar2
Address = 10.2.0.31/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_ar2
AllowedIPs = 0.0.0.0/0
Endpoint = 31.31.31.31:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Create category
run_vpn_switch wireguard add performance >/dev/null 2>&1
# Run add with arity-2 (category + config)
local output=$(run_vpn_switch wireguard add performance wg-TEST-AR2-1.conf 2>&1 || true)
# Should delegate to add3 with config name as alias
# Look for success message showing target used as alias
if printf '%s\n' "$output" | grep -qE "Added to.*performance.*wg-TEST-AR2-1.conf.*->.*wg-TEST-AR2-1.conf|wg-TEST-AR2-1.conf.*wg-TEST-AR2-1.conf"; then
pass "Arity-2 delegates to add3 with target as alias"
else
fail "Delegation to add3 not working"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 43: WireGuard add - arity-3 with custom alias
test_wireguard_add_link_arity3() {
test_header "wireguard add - arity-3 with custom alias" "test_wireguard_add_category_conflict"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard import and add (narrow overrides)
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "wireguard_add" "sh"
# Import a config first
local test_config="$TEST_BASE_DIR/wg-TEST-AR3-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_ar3
Address = 10.2.0.32/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_ar3
AllowedIPs = 0.0.0.0/0
Endpoint = 32.32.32.32:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Create category
run_vpn_switch wireguard add streaming >/dev/null 2>&1
# Run add with arity-3 (category + config + alias)
local output=$(run_vpn_switch wireguard add streaming wg-TEST-AR3-1.conf swiss 2>&1 || true)
# Verify success message with alias and target
if printf '%s\n' "$output" | grep -qE "Added to.*streaming.*swiss.*->.*wg-TEST-AR3-1.conf|swiss.*wg-TEST-AR3-1.conf"; then
pass "Link created with custom alias (swiss)"
else
fail "Link creation failed"
printf '%s\n' "Output: $output"
fi
# Verify symlink was actually created (if execution enabled)
if [ -L "$TEST_DIR/wireguard/streaming/swiss" ]; then
pass "Symlink file created at streaming/swiss"
else
# Not a failure if just showing commands
pass "Success message with alias displayed"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 44: WireGuard add - error when link already exists
test_wireguard_add_link_conflict() {
test_header "wireguard add - error when link already exists" "test_wireguard_add_link_conflict"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard import and add (narrow overrides)
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "wireguard_add" "sh"
# Import a config
local test_config="$TEST_BASE_DIR/wg-TEST-CONF-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_conf1
Address = 10.2.0.33/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_conf1
AllowedIPs = 0.0.0.0/0
Endpoint = 33.33.33.33:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Create category and add link
run_vpn_switch wireguard add privacy >/dev/null 2>&1
run_vpn_switch wireguard add privacy wg-TEST-CONF-1.conf link1 >/dev/null 2>&1
# Verify link was created
if [ -L "$TEST_DIR/wireguard/privacy/link1" ]; then
pass "First link created successfully"
else
fail "First link not created"
return
fi
# Try to add the same link again (conflict)
local output=$(run_vpn_switch wireguard add privacy wg-TEST-CONF-1.conf link1 2>&1 || true)
# Verify error message
if printf '%s\n' "$output" | grep -qiE "error.*already exists|alias.*exists|link.*exists"; then
pass "Error message when link already exists"
else
fail "Missing error for existing link"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 45: WireGuard link - error when alias already exists
test_wireguard_link_conflict() {
test_header "wireguard link - error when alias already exists" "test_wireguard_link_conflict"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard import (narrow override)
set_function_interpreter "wireguard_import1" "sh"
# Import a config
local test_config="$TEST_BASE_DIR/wg-TEST-LINK-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_link1
Address = 10.2.0.34/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_link1
AllowedIPs = 0.0.0.0/0
Endpoint = 34.34.34.34:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Verify config exists
if [ -f "$TEST_DIR/wireguard/wg-TEST-LINK-1.conf" ]; then
pass "Config imported successfully"
else
fail "Config import failed"
rm -f "$test_config"
return
fi
# Try to create link with same name as existing config file (conflict)
local output=$(run_vpn_switch wireguard link wg-TEST-LINK-1.conf wg-TEST-LINK-1.conf 2>&1 || true)
# Verify error message for existing alias
if printf '%s\n' "$output" | grep -qiE "error.*already exists|alias.*exists|conflict"; then
pass "Error message when alias already exists"
else
fail "Missing error for existing alias"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 46: Error handling - invalid category
test_error_invalid_category() {
test_header "error handling - invalid category shows error" "test_error_invalid_category"
# Create exclusive database for this test
test_setup
# Try to start VPN with nonexistent category
local output=$(run_vpn_switch wireguard start nonexistent 2>&1 || true)
# Verify error message displayed
if printf '%s\n' "$output" | grep -qiE "error.*not found|category not found|no.*found"; then
pass "Error message for invalid category"
else
fail "Missing error message for invalid category"
printf '%s\n' "Output: $output"
fi
}
# TEST 16: Error handling - broken symlink
test_error_broken_symlink() {
test_header "error handling - broken symlink handled gracefully" "test_error_broken_symlink"
# Create exclusive database for this test
test_setup
# Enable execution for wireguard add (narrow override)
set_function_interpreter "wireguard_add" "sh"
# Create privacy category
run_vpn_switch wireguard add privacy >/dev/null 2>&1
# Create a broken symlink in privacy category
ln -s ../nonexistent.conf "$TEST_DIR/wireguard/privacy/broken" 2>/dev/null || true
# Verify broken symlink exists
if [ -L "$TEST_DIR/wireguard/privacy/broken" ] && [ ! -e "$TEST_DIR/wireguard/privacy/broken" ]; then
pass "Broken symlink created for testing"
else
fail "Failed to create broken symlink"
ls -la "$TEST_DIR/wireguard/privacy/" || true
fi
# Try to start VPN with category containing broken symlink
local output=$(run_vpn_switch wireguard start privacy 2>&1 || true)
# Should either skip broken link or error gracefully
if printf '%s\n' "$output" | grep -qiE "error|selected config|no.*found|not found"; then
pass "Handled broken symlink (error or skipped)"
else
fail "Did not handle broken symlink gracefully"
printf '%s\n' "Output: $output"
fi
}
# TEST 17: Error handling - invalid command
test_error_invalid_command() {
test_header "error handling - invalid command shows usage" "test_error_invalid_command"
# Create exclusive database for this test
test_setup
# Try to run invalid command
local output=$(run_vpn_switch invalid_command_xyz123 2>&1 || true)
# Verify usage or help displayed
if printf '%s\n' "$output" | grep -qiE "usage|help|unknown"; then
pass "Usage/help shown for invalid command"
else
fail "No usage/help for invalid command"
printf '%s\n' "Output: $output"
fi
}
# TEST 18: Edge case - duplicate instance prevention
test_edge_duplicate_instance_prevention() {
test_header "edge case - prevent duplicate VPN instances" "test_edge_duplicate_instance_prevention"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations with unique test ID
local test_id=53
setup_session_test_mocks "$test_id"
local test_interface="wg_test${test_id}"
# Enable execution for wireguard add and import (narrow overrides)
set_function_interpreter "wireguard_add" "sh"
set_function_interpreter "wireguard_import1" "sh"
# Create privacy category and config
run_vpn_switch wireguard add privacy >/dev/null 2>&1
local test_config="$TEST_BASE_DIR/wg-TEST-DUP-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_dup1
Address = 10.2.0.99/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_dup1
AllowedIPs = 0.0.0.0/0
Endpoint = 99.99.99.99:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
run_vpn_switch wireguard add privacy wg-TEST-DUP-1.conf dup1 >/dev/null 2>&1
# Create first "active" session (one with a running process PID)
# This documents what makes a session "active": having a PID that corresponds to a running process
# In a mocked environment (where wg-quick is echo'd), using wireguard start would create a
# session without a real VPN process, so it would be detected as stale and auto-removed
local first_pid=$$ # Use current shell PID (definitely running)
local first_session="$TEST_DIR/.session/$first_pid"
mkdir -p "$first_session"
echo "$first_pid" > "$first_session/pid"
echo "wireguard" > "$first_session/protocol"
echo "$TEST_DIR/wireguard/privacy/dup1" > "$first_session/original"
echo "$test_interface" > "$first_session/interface"
date "+%Y-%m-%d %H:%M:%S" > "$first_session/started"
echo "$first_pid" > "$first_session/vpn.pid" # VPN process PID (must be running for duplicate detection)
if [ -d "$first_session" ]; then
pass "First VPN session created (PID $first_pid)"
else
fail "First VPN session not created"
return
fi
# Try to start VPN again - should detect existing session and refuse
local output=$(run_vpn_switch wireguard start privacy 2>&1 || true)
# Should detect existing instance
if printf '%s\n' "$output" | grep -qiE "already running|already exists|already active"; then
pass "Detected existing VPN instance"
else
fail "Did not detect existing VPN instance"
printf '%s\n' "Output: $output"
fi
# Verify no duplicate session created (should still be only one)
local session_count=$(ls -1d "$TEST_DIR/.session"/* 2>/dev/null | wc -l)
if [ "$session_count" -eq 1 ]; then
pass "No duplicate session created"
else
fail "Duplicate session created (found $session_count sessions)"
ls -la "$TEST_DIR/.session/" || true
fi
# Cleanup
rm -f "$test_config"
}
# TEST 19: Edge case - stale sessions don't block new sessions
test_edge_stale_session_cleanup() {
test_header "edge case - stale sessions don't block new sessions" "test_edge_stale_session_cleanup"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations
setup_session_test_mocks
# Enable execution for wireguard add and import (narrow overrides)
set_function_interpreter "wireguard_add" "sh"
set_function_interpreter "wireguard_import1" "sh"
# Create privacy category and config
run_vpn_switch wireguard add privacy >/dev/null 2>&1
local test_config="$TEST_BASE_DIR/wg-TEST-STALE-1.conf"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test_key_stale1
Address = 10.2.0.88/32
DNS = 10.2.0.1
[Peer]
PublicKey = test_public_key_stale1
AllowedIPs = 0.0.0.0/0
Endpoint = 88.88.88.88:51820
EOF
chmod 0600 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
run_vpn_switch wireguard add privacy wg-TEST-STALE-1.conf stale1 >/dev/null 2>&1
# Create a stale session with dead PID (99998 definitely doesn't exist)
local dead_pid=99998
local session_dir="$TEST_DIR/.session/$dead_pid"
mkdir -p "$session_dir"
echo "$dead_pid" > "$session_dir/pid"
echo "wireguard" > "$session_dir/protocol"
echo "$TEST_DIR/wireguard/privacy/stale1" > "$session_dir/original"
echo "wg_test3" > "$session_dir/interface"
date "+%Y-%m-%d %H:%M:%S" > "$session_dir/started"
echo "99994" > "$session_dir/vpn.pid" # Dead VPN process PID (stale session)
# Verify stale session exists
if [ -d "$session_dir" ]; then
pass "Stale session created for testing"
else
fail "Failed to create stale session"
fi
# Try to start VPN - should proceed (stale sessions don't block new sessions)
# Note: In unit test mode (dry-run), session won't actually be created,
# but the command should produce output without errors about stale sessions
local output=$(run_vpn_switch wireguard start privacy 2>&1 || true)
# Should proceed with connection (stale sessions don't prevent new ones)
if printf '%s\n' "$output" | grep -qiE "session|wg-quick"; then
pass "Proceeded with new session (stale session didn't block)"
else
fail "Did not proceed (should allow new session despite stale one)"
printf '%s\n' "Output: $output"
fi
# Verify stale session was NOT automatically cleaned up
# (Stale cleanup is now manual via 'session clean' command)
if [ -d "$session_dir" ]; then
pass "Stale session preserved (manual cleanup via 'session clean' required)"
else
fail "Stale session was unexpectedly removed"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 51: wireguard patch - displays patched configuration
test_wireguard_patch_valid() {
test_header "wireguard patch - displays patched configuration" "test_wireguard_patch_valid"
test_setup
override_for_command_inspection "wireguard_patch"
# Enable execution for wireguard import (narrow override)
set_function_interpreter "wireguard_import1" "sh"
# Create a test config
local test_config="/tmp/wg-TEST-PATCH-1.conf"
# Cleanup first (in case previous run failed before cleanup)
rm -f "$test_config"
cat > "$test_config" <<'CONF'
[Interface]
PrivateKey = test123
Address = 10.2.0.2/32
DNS = 10.2.0.1
[Peer]
PublicKey = server123
AllowedIPs = 0.0.0.0/0
Endpoint = 1.2.3.4:51820
CONF
chmod 0400 "$test_config"
# Import config first
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Run patch command
local output=$(run_vpn_switch wireguard patch wg-TEST-PATCH-1.conf 2>&1)
# Phase-based approach: DNS handled by DNS phase, not PostUp/PostDown hooks
# Verify PostUp/PostDown hooks are NOT present (they should be stripped)
if printf '%s\n' "$output" | grep -q "PostUp.*resolvconf"; then
fail "Patch output should NOT contain PostUp hook (phase-based DNS)"
else
pass "Patch output correctly strips PostUp hook (phase-based DNS)"
fi
if printf '%s\n' "$output" | grep -q "PostDown.*resolvconf"; then
fail "Patch output should NOT contain PostDown hook (phase-based DNS)"
else
pass "Patch output correctly strips PostDown hook (phase-based DNS)"
fi
# Verify output still contains original config elements
if printf '%s\n' "$output" | grep -q "\[Interface\]"; then
pass "Patch output preserves [Interface] section"
else
fail "Patch output lost [Interface] section"
fi
if printf '%s\n' "$output" | grep -q "\[Peer\]"; then
pass "Patch output preserves [Peer] section"
else
fail "Patch output lost [Peer] section"
fi
# Keepalive injected (source config has none; default is 25)
if printf '%s\n' "$output" | grep -q "^PersistentKeepalive = 25$"; then
pass "Patch output injects PersistentKeepalive (default 25)"
else
fail "Patch output should inject 'PersistentKeepalive = 25'"
printf '%s\n' "Output: $output"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 52: wireguard patch - handles nonexistent config
test_wireguard_patch_nonexistent() {
test_header "wireguard patch - handles nonexistent config" "test_wireguard_patch_nonexistent"
test_setup
# Try to patch non-existent config
local output=$(run_vpn_switch wireguard patch nonexistent-config.conf 2>&1 || true)
if printf '%s\n' "$output" | grep -qiE "not found|does not exist"; then
pass "Patch rejects nonexistent config with error"
else
fail "Patch should reject nonexistent config"
fi
}
# TEST 53: wireguard patch - handles absolute path
test_wireguard_patch_absolute() {
test_header "wireguard patch - handles absolute path" "test_wireguard_patch_absolute"
test_setup
override_for_command_inspection "wireguard_patch"
# Enable execution for wireguard import (narrow override)
set_function_interpreter "wireguard_import1" "sh"
# Create a test config with absolute path
local test_config="/tmp/wg-TEST-PATCH-ABS.conf"
# Cleanup first (in case previous run failed before cleanup)
rm -f "$test_config"
cat > "$test_config" <<'CONF'
[Interface]
PrivateKey = test456
Address = 10.2.0.3/32
DNS = 10.2.0.1
CONF
chmod 0400 "$test_config"
# Import config first
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Run patch with absolute path
local output=$(run_vpn_switch wireguard patch "$TEST_DIR/wireguard/wg-TEST-PATCH-ABS.conf" 2>&1)
# Phase-based approach: DNS handled by DNS phase, not PostUp hooks
if printf '%s\n' "$output" | grep -q "PostUp.*resolvconf"; then
fail "Patch should NOT contain PostUp hook (phase-based DNS)"
else
pass "Patch works with absolute path (strips DNS hooks)"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 53a: wireguard patch - PersistentKeepalive injection rules
test_wireguard_patch_keepalive() {
test_header "wireguard patch - PersistentKeepalive injection rules" "test_wireguard_patch_keepalive"
test_setup
override_for_command_inspection "wireguard_patch"
# Enable execution for wireguard import (narrow override)
set_function_interpreter "wireguard_import1" "sh"
# Config that sets PersistentKeepalive itself - the explicit value must win
local test_config="/tmp/wg-TEST-KEEPALIVE-1.conf"
# Cleanup first (in case previous run failed before cleanup)
rm -f "$test_config"
cat > "$test_config" <<'CONF'
[Interface]
PrivateKey = test123
Address = 10.2.0.2/32
[Peer]
PublicKey = server123
AllowedIPs = 0.0.0.0/0
Endpoint = 1.2.3.4:51820
PersistentKeepalive = 7
CONF
chmod 0400 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
local output=$(run_vpn_switch wireguard patch wg-TEST-KEEPALIVE-1.conf 2>&1)
if printf '%s\n' "$output" | grep -q "^PersistentKeepalive = 7$"; then
pass "Explicit PersistentKeepalive from config preserved"
else
fail "Explicit PersistentKeepalive from config lost"
printf '%s\n' "Output: $output"
fi
if printf '%s\n' "$output" | grep -q "^PersistentKeepalive = 25$"; then
fail "Default keepalive must not be injected next to an explicit one"
printf '%s\n' "Output: $output"
else
pass "No duplicate keepalive injected"
fi
# VPN_SWITCH_KEEPALIVE_wireguard=0 disables injection entirely
local test_config2="/tmp/wg-TEST-KEEPALIVE-2.conf"
rm -f "$test_config2"
cat > "$test_config2" <<'CONF'
[Interface]
PrivateKey = test123
Address = 10.2.0.2/32
[Peer]
PublicKey = server123
AllowedIPs = 0.0.0.0/0
Endpoint = 1.2.3.4:51820
CONF
chmod 0400 "$test_config2"
run_vpn_switch wireguard import "$test_config2" >/dev/null 2>&1
run_vpn_switch setenv VPN_SWITCH_KEEPALIVE_wireguard 0 >/dev/null 2>&1
output=$(run_vpn_switch wireguard patch wg-TEST-KEEPALIVE-2.conf 2>&1)
run_vpn_switch unsetenv VPN_SWITCH_KEEPALIVE_wireguard >/dev/null 2>&1
if printf '%s\n' "$output" | grep -qi "PersistentKeepalive"; then
fail "Keepalive injected although VPN_SWITCH_KEEPALIVE_wireguard=0"
printf '%s\n' "Output: $output"
else
pass "VPN_SWITCH_KEEPALIVE_wireguard=0 disables injection"
fi
# Cleanup
rm -f "$test_config" "$test_config2"
}
# TEST 54: wireguard info - displays category information
test_wireguard_info_category() {
test_header "wireguard info - displays category information" "test_wireguard_info_category"
test_setup
# Enable execution for wireguard import and add (narrow override)
set_function_interpreter "wireguard_add" "sh"
set_function_interpreter "wireguard_import1" "sh"
# Create a test config and category
local test_config="/tmp/wg-TEST-INFO-1.conf"
# Cleanup first (in case previous run failed before cleanup)
rm -f "$test_config"
cat > "$test_config" <<'CONF'
[Interface]
PrivateKey = testinfo123
Address = 10.2.0.5/32
DNS = 10.2.0.1
CONF
chmod 0400 "$test_config"
# Import config and create category with links
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
run_vpn_switch wireguard add testinfo >/dev/null 2>&1
run_vpn_switch wireguard add testinfo wg-TEST-INFO-1.conf >/dev/null 2>&1
run_vpn_switch wireguard add testinfo wg-TEST-INFO-1.conf info1 >/dev/null 2>&1
# Run info command on category
local output=$(run_vpn_switch wireguard info testinfo 2>&1 || true)
# Verify category header displayed
if printf '%s\n' "$output" | grep -q "# Category: testinfo"; then
pass "Category header displayed"
else
fail "Category header missing"
fi
# Verify counts section
if printf '%s\n' "$output" | grep -q "# Configs:"; then
pass "Config count displayed"
else
fail "Config count missing"
fi
if printf '%s\n' "$output" | grep -q "# Links:"; then
pass "Link count displayed"
else
fail "Link count missing"
fi
# Verify path displayed
if printf '%s\n' "$output" | grep -q "# Path:.*testinfo"; then
pass "Category path displayed"
else
fail "Category path missing"
fi
# Verify contents section
if printf '%s\n' "$output" | grep -q "# Contents:"; then
pass "Contents section header displayed"
else
fail "Contents section missing"
fi
# Verify symlinks shown (should show ->)
if printf '%s\n' "$output" | grep -q -- "->"; then
pass "Category contents listed with symlink arrows"
else
fail "Contents not shown properly"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 55: session save - save current session as default
test_session_save_default() {
test_header "session save - save current session as default" "test_session_save_default"
test_setup
# Set up mocks for network operations (wg-quick, pfctl, wg9 interface)
setup_session_test_mocks
# Enable execution for wireguard import and session save (narrow override)
# Note: wireguard start is a combinator function - uses default interpreter
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "session_save" "sh"
# Create and import a test config
local test_config="/tmp/wg-TEST-SAVE-DEFAULT.conf"
# Cleanup first (in case previous run failed before cleanup)
rm -f "$test_config"
cat > "$test_config" <<'CONF'
[Interface]
PrivateKey = testsavedefault123
Address = 10.2.0.10/32
DNS = 10.2.0.1
CONF
chmod 0400 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Start a session (creates active session)
local start_output=$(run_vpn_switch wireguard start wg-TEST-SAVE-DEFAULT.conf 2>&1)
local session_id=$(echo "$start_output" | grep -oE '[Ss]ession:? [0-9]+|PID:? [0-9]+|session [0-9]+' | grep -oE '[0-9]+' | head -1)
# Save session as default
run_vpn_switch session save >/dev/null 2>&1
# Verify default symlink was created
if [ -L "$TEST_DIR/session/default" ]; then
pass "Default session symlink created"
else
fail "Default session symlink not created"
fi
# Verify symlink points to correct session
local link_target=$(readlink "$TEST_DIR/session/default")
if echo "$link_target" | grep -q "$session_id"; then
pass "Default symlink points to correct session"
else
fail "Default symlink points to wrong session"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 56: session save - save with custom name
test_session_save_named() {
test_header "session save work - save with custom name" "test_session_save_named"
test_setup
# Set up mocks for network operations (wg-quick, pfctl, wg9 interface)
setup_session_test_mocks
# Enable execution for wireguard import and session save (narrow override)
# Note: wireguard start is a combinator function - uses default interpreter
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "session_save" "sh"
# Create and import a test config
local test_config="/tmp/wg-TEST-SAVE-NAMED.conf"
# Cleanup first (in case previous run failed before cleanup)
rm -f "$test_config"
cat > "$test_config" <<'CONF'
[Interface]
PrivateKey = testsavenamed456
Address = 10.2.0.11/32
DNS = 10.2.0.1
CONF
chmod 0400 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Start a session
local start_output=$(run_vpn_switch wireguard start wg-TEST-SAVE-NAMED.conf 2>&1)
local session_id=$(echo "$start_output" | grep -oE '[Ss]ession:? [0-9]+|PID:? [0-9]+|session [0-9]+' | grep -oE '[0-9]+' | head -1)
# Save session with custom name "work"
run_vpn_switch session save work >/dev/null 2>&1
# Verify named symlink was created
if [ -L "$TEST_DIR/session/work" ]; then
pass "Named session symlink created"
else
fail "Named session symlink not created"
fi
# Verify symlink points to correct session
local link_target=$(readlink "$TEST_DIR/session/work")
if echo "$link_target" | grep -q "$session_id"; then
pass "Named symlink points to correct session"
else
fail "Named symlink points to wrong session"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 57: session start - resume default session
test_session_start_default() {
test_header "session start - resume default session" "test_session_start_default"
test_setup
# Set up mocks for network operations (wg-quick, pfctl, wg9 interface)
setup_session_test_mocks
# Enable execution for terminal commands (narrow override)
# Note: wireguard start and session start are combinator functions - use default interpreter
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "session_save" "sh"
set_function_interpreter "session_stop" "sh"
# Create and import a test config
local test_config="/tmp/wg-TEST-START-DEFAULT.conf"
# Cleanup first (in case previous run failed before cleanup)
rm -f "$test_config"
cat > "$test_config" <<'CONF'
[Interface]
PrivateKey = teststartdefault789
Address = 10.2.0.12/32
DNS = 10.2.0.1
CONF
chmod 0400 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Start a session and save as default
run_vpn_switch wireguard start wg-TEST-START-DEFAULT.conf >/dev/null 2>&1
local session_id=$(ls -t "$TEST_DIR/.session" | head -1)
run_vpn_switch session save >/dev/null 2>&1
# Verify connect.sh exists before stopping
if [ -f "$TEST_DIR/.session/$session_id/connect.sh" ]; then
pass "Connect script created before stopping"
else
fail "Connect script not found"
fi
# Stop the session
run_vpn_switch stop >/dev/null 2>&1
# Verify session was stopped but session data remains (because it's saved/referenced)
if [ -d "$TEST_DIR/.session/$session_id" ]; then
pass "Session directory kept for resumption (referenced)"
else
fail "Session directory was removed (should be kept for resumption)"
fi
if [ -L "$TEST_DIR/session/default" ]; then
pass "Default symlink persists after stop"
else
fail "Default symlink was removed"
fi
# Resume default session
local resume_output=$(run_vpn_switch session start 2>&1)
# Verify resumption message or success
if echo "$resume_output" | grep -qiE "resuming|starting|session"; then
pass "Session start executed successfully"
else
fail "Session start failed: $resume_output"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 58: session start - resume named session
test_session_start_named() {
test_header "session start work - resume named session" "test_session_start_named"
test_setup
# Set up mocks for network operations (wg-quick, pfctl, wg9 interface)
setup_session_test_mocks
# Enable execution for terminal commands (narrow override)
# Note: wireguard start and session start are combinator functions - use default interpreter
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "session_save" "sh"
set_function_interpreter "session_stop" "sh"
# Create and import a test config
local test_config="/tmp/wg-TEST-START-NAMED.conf"
rm -f "$test_config" # Remove if exists from previous run
cat > "$test_config" <<'CONF'
[Interface]
PrivateKey = teststartnamed012
Address = 10.2.0.13/32
DNS = 10.2.0.1
CONF
chmod 0400 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Start a session and save with name "work"
run_vpn_switch wireguard start wg-TEST-START-NAMED.conf >/dev/null 2>&1
run_vpn_switch session save work >/dev/null 2>&1
# Stop the session
run_vpn_switch stop >/dev/null 2>&1
# Verify named symlink still exists
if [ -L "$TEST_DIR/session/work" ]; then
pass "Named symlink persists after stop"
else
fail "Named symlink was removed"
fi
# Resume named session
local resume_output=$(run_vpn_switch session start work 2>&1)
# Verify resumption
if echo "$resume_output" | grep -qiE "resuming|starting|session|work"; then
pass "Named session start executed successfully"
else
fail "Named session start failed: $resume_output"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 59: session start - resume by PID
test_session_start_by_pid() {
test_header "session start <PID> - resume by PID" "test_session_start_by_pid"
test_setup
# Set up mocks for network operations (wg-quick, pfctl, wg9 interface)
setup_session_test_mocks
# Enable execution for terminal commands (narrow override)
# Note: wireguard start and session start are combinator functions - use default interpreter
set_function_interpreter "wireguard_import1" "sh"
set_function_interpreter "session_save" "sh"
set_function_interpreter "session_stop" "sh"
# Create and import a test config
local test_config="/tmp/wg-TEST-START-PID.conf"
# Cleanup first (in case previous run failed before cleanup)
rm -f "$test_config"
cat > "$test_config" <<'CONF'
[Interface]
PrivateKey = teststartpid345
Address = 10.2.0.14/32
DNS = 10.2.0.1
CONF
chmod 0400 "$test_config"
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Start a session and capture PID
local start_output=$(run_vpn_switch wireguard start wg-TEST-START-PID.conf 2>&1)
local session_id=$(echo "$start_output" | grep -oE '[Ss]ession:? [0-9]+|PID:? [0-9]+|session [0-9]+' | grep -oE '[0-9]+' | head -1)
# Save session (to preserve connect.sh)
run_vpn_switch session save pid-test >/dev/null 2>&1
# Stop the session
run_vpn_switch stop >/dev/null 2>&1
# Resume by explicit PID
local resume_output=$(run_vpn_switch session start "$session_id" 2>&1)
# Verify resumption by PID
if echo "$resume_output" | grep -qiE "resuming|starting|session|$session_id"; then
pass "Session start by PID executed successfully"
else
fail "Session start by PID failed: $resume_output"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 60: Protocol-agnostic import - detects WireGuard config
test_protocol_agnostic_import_wireguard() {
test_header "import (protocol-agnostic) - detects WireGuard .conf file" "test_protocol_agnostic_import_wireguard"
test_setup
# Override wireguard_import to cat (profile-independent)
override_for_command_inspection "wireguard_import1"
# Create test WireGuard config
local test_config="/tmp/wg-PROTO-IMPORT.conf"
# Cleanup first (in case previous run failed before cleanup)
rm -f "$test_config"
cat > "$test_config" <<'CONF'
[Interface]
PrivateKey = protoimporttest123
Address = 10.2.0.20/32
DNS = 10.2.0.1
CONF
chmod 0400 "$test_config"
# Test protocol-agnostic import (combinator delegates to wireguard import)
local output=$(run_vpn_switch import "$test_config" 2>&1)
# Verify wrapper detected .conf and delegated to wireguard import
if printf '%s\n' "$output" | grep -q "# Import WireGuard config"; then
pass "Protocol-agnostic import detected .conf and delegated to wireguard import"
else
fail "Protocol-agnostic import failed to delegate: $output"
fi
# Verify output contains cp command with correct paths
if printf '%s\n' "$output" | grep -q "cp.*$test_config.*wireguard/wg-PROTO-IMPORT.conf"; then
pass "Protocol-agnostic import generates correct cp command"
else
fail "Protocol-agnostic import missing correct cp command"
fi
# Cleanup
rm -f "$test_config"
}
# TEST 61: Protocol-agnostic list - delegates to protocol-specific lists
test_protocol_agnostic_list() {
test_header "list (protocol-agnostic) - delegates to all protocols" "test_protocol_agnostic_list"
test_setup
# Leave VPN_SWITCH_INTERPRETER_wireguard_list as default (cat) - we test output
# Test protocol-agnostic list (combinator delegates to wireguard list)
local output=$(run_vpn_switch list 2>&1)
# Verify wrapper delegated to wireguard list (check for WireGuard header)
if printf '%s\n' "$output" | grep -q "WireGuard Configurations"; then
pass "Protocol-agnostic list delegated to wireguard list"
else
fail "Protocol-agnostic list failed to delegate: $output"
fi
# Verify output shows configs section (check for "Configuration files:")
if printf '%s\n' "$output" | grep -q "Configuration files:"; then
pass "Protocol-agnostic list shows wireguard config sections"
else
fail "Protocol-agnostic list missing config sections"
fi
# Verify no duplicate entries (the section labels repeat per protocol; an entry names a path)
local total_lines=$(printf '%s\n' "$output" | grep -v '^[ \t]*$' | grep -v '#' | grep '/' | wc -l | tr -d ' ')
local unique_lines=$(printf '%s\n' "$output" | grep -v '^[ \t]*$' | grep -v '#' | grep '/' | sort -u | wc -l | tr -d ' ')
if [ "$total_lines" = "$unique_lines" ]; then
pass "Protocol-agnostic list has no duplicate lines ($total_lines unique)"
else
fail "Protocol-agnostic list has duplicates" "$total_lines unique lines" "$unique_lines after deduplication"
fi
}
# TEST 62: Protocol-agnostic link - creates protocol-level alias
test_protocol_agnostic_link() {
test_header "link (protocol-agnostic) - creates protocol-level alias" "test_protocol_agnostic_link"
test_setup
# Override interpreter to inspect link command output (profile-independent)
override_for_command_inspection "link"
# _link2 is a leaf function - test its output without execution
# Protocol-level link creates: $BASE/alias -> $BASE/protocol
# Test link command generation
local output=$(run_vpn_switch link testproto wireguard 2>&1 || true)
# Verify output contains ln command
if printf '%s\n' "$output" | grep -q "ln -s"; then
pass "Protocol-agnostic link generates ln command"
else
fail "Protocol-agnostic link missing ln command: $output"
fi
# Verify relative path (./wireguard)
if printf '%s\n' "$output" | grep -q './wireguard'; then
pass "Protocol-agnostic link uses correct relative path (./wireguard)"
else
fail "Protocol-agnostic link has incorrect path"
fi
# Verify target path includes testproto
if printf '%s\n' "$output" | grep -q "testproto"; then
pass "Protocol-agnostic link includes correct alias name"
else
fail "Protocol-agnostic link missing alias name"
fi
}
# TEST 63: Protocol-agnostic remove - removes protocol-level link
test_protocol_agnostic_remove() {
test_header "remove (protocol-agnostic) - removes protocol-level link" "test_protocol_agnostic_remove"
test_setup
# Override interpreter to inspect remove command output (profile-independent)
override_for_command_inspection "remove"
# _remove1 is a leaf function - test its output without execution
# First create a dummy symlink for testing
ln -s "./wireguard" "$TEST_DIR/testremove"
# Test remove command generation
local output=$(run_vpn_switch remove testremove 2>&1)
# Verify output contains rm command
if printf '%s\n' "$output" | grep -q "rm.*testremove"; then
pass "Protocol-agnostic remove generates rm command"
else
fail "Protocol-agnostic remove missing rm command: $output"
fi
# Verify comment about removing link
if printf '%s\n' "$output" | grep -qiE "remove.*link|remove.*protocol"; then
pass "Protocol-agnostic remove includes descriptive comment"
else
fail "Protocol-agnostic remove missing descriptive comment"
fi
# Test error on non-existent link (should get error, not commands)
local err_output=$(run_vpn_switch remove nonexistent 2>&1 || true)
if echo "$err_output" | grep -qiE "not found|error"; then
pass "Protocol-agnostic remove errors on non-existent link"
else
fail "Protocol-agnostic remove should error on non-existent link"
fi
}
# TEST 64: Protocol-agnostic validate - delegates to protocol-specific validate
test_protocol_agnostic_validate() {
test_header "validate (protocol-agnostic) - delegates to all protocols" "test_protocol_agnostic_validate"
test_setup
# The validate pins strip the leading '# ' (cut -b3-) -- we test the output
# Test protocol-agnostic validate (combinator delegates to wireguard validate)
local output=$(run_vpn_switch validate 2>&1)
# Verify wrapper delegated to wireguard validate (check for validation header)
if printf '%s\n' "$output" | grep -q "WireGuard Configuration Validation"; then
pass "Protocol-agnostic validate delegated to wireguard validate"
else
fail "Protocol-agnostic validate failed to delegate: $output"
fi
# Verify output shows validation sections
if printf '%s\n' "$output" | grep -qE "(^|# )(Checking|Validation)"; then
pass "Protocol-agnostic validate shows validation output"
else
fail "Protocol-agnostic validate missing validation output"
fi
}
#-----------------------------------------------------------------------------
# System Validation Tests (Phase 4.1 - Feature 3)
#-----------------------------------------------------------------------------
# TEST 65: Network validate - checks network prerequisites
test_network_validate() {
test_header "network validate - check network prerequisites" "test_network_validate"
test_setup
# Run network validate
local output=$(run_vpn_switch network validate)
# Verify validation header
if printf '%s\n' "$output" | grep -q "Network Prerequisites Validation"; then
pass "Network validation header displayed"
else
fail "Network validation header missing"
printf '%s\n' "Output: $output"
fi
# Verify summary on clean configuration.
# Host-dependent findings (missing external interface, stale VPN interface)
# are warnings, not errors — accept the warnings summary as passing.
if printf '%s\n' "$output" | grep -q -e "Network prerequisites OK" -e "warning(s)"; then
pass "Network validate passed on clean configuration"
else
fail "Network validate did not pass"
printf '%s\n' "Output: $output"
fi
}
# TEST 66: Permissions validate - checks file/directory permissions
test_permissions_validate() {
test_header "permissions validate - check filesystem permissions" "test_permissions_validate"
test_setup
# Run permissions validate
local output=$(run_vpn_switch permissions validate)
# Verify validation header
if printf '%s\n' "$output" | grep -q "Permissions Validation"; then
pass "Permissions validation header displayed"
else
fail "Permissions validation header missing"
printf '%s\n' "Output: $output"
fi
# Verify summary on clean configuration.
# Permission drift in the test DB is a warning, not an error —
# accept the warnings summary as passing.
if printf '%s\n' "$output" | grep -q -e "Permissions OK" -e "permission warning(s)"; then
pass "Permissions validate passed on clean configuration"
else
fail "Permissions validate did not pass"
printf '%s\n' "Output: $output"
fi
}
# TEST 67: Environment validate - checks environment configuration
test_environment_validate() {
test_header "environment validate - check environment variables" "test_environment_validate"
test_setup
# Run environment validate
local output=$(run_vpn_switch environment validate)
# Verify validation header
if printf '%s\n' "$output" | grep -q "Environment Configuration Validation"; then
pass "Environment validation header displayed"
else
fail "Environment validation header missing"
printf '%s\n' "Output: $output"
fi
# Verify summary on clean configuration.
# Unset optional variables are warnings, not errors — accept the
# warnings summary as passing.
if printf '%s\n' "$output" | grep -q -e "Environment configuration OK" -e "configuration warning(s)"; then
pass "Environment validate passed on clean configuration"
else
fail "Environment validate did not pass"
printf '%s\n' "Output: $output"
fi
}
# TEST 68: Batch validate - includes all system validators
test_batch_validate_includes_system() {
test_header "validate (batch) - includes system validators" "test_batch_validate_includes_system"
test_setup
# Run batch validate (batch combinator)
local output=$(run_vpn_switch validate 2>&1)
# Verify it includes database validate
if printf '%s\n' "$output" | grep -q "Database Structure Validation"; then
pass "Batch validate includes database validator"
else
fail "Batch validate missing database validator"
fi
# Verify it includes binaries validate
if printf '%s\n' "$output" | grep -q "Binary Availability Validation"; then
pass "Batch validate includes binaries validator"
else
fail "Batch validate missing binaries validator"
fi
# Verify it includes phases validate
if printf '%s\n' "$output" | grep -q "Phase System Validation"; then
pass "Batch validate includes phases validator"
else
fail "Batch validate missing phases validator"
fi
# Verify it includes network validate
if printf '%s\n' "$output" | grep -q "Network Prerequisites Validation"; then
pass "Batch validate includes network validator"
else
fail "Batch validate missing network validator"
fi
# Verify it includes permissions validate
if printf '%s\n' "$output" | grep -q "Permissions Validation"; then
pass "Batch validate includes permissions validator"
else
fail "Batch validate missing permissions validator"
fi
# Verify it includes environment validate
if printf '%s\n' "$output" | grep -q "Environment Configuration Validation"; then
pass "Batch validate includes environment validator"
else
fail "Batch validate missing environment validator"
fi
}
#-----------------------------------------------------------------------------
# Interpreter Resolution Tests (Lower Half of lookup_interpreter)
#-----------------------------------------------------------------------------
# TEST 65: Single underscore → TERMINAL_INTERPRETER (intrinsic classification)
test_interpreter_single_underscore() {
test_header "interpreter resolution - single underscore uses TERMINAL_INTERPRETER" "test_interpreter_single_underscore"
test_setup
# Set TERMINAL_INTERPRETER to distinctive value (NOT cat, NOT sh)
# This tests the lower half of lookup_interpreter() (intrinsic classification)
run_vpn_switch setenv "VPN_SWITCH_TERMINAL_INTERPRETER" "echo TERMINAL_FIRED"
# IMPORTANT: Remove function-specific overrides that would block intrinsic classification
# The profile may include VPN_SWITCH_INTERPRETER_wireguard_list (arity-less)
# and VPN_SWITCH_INTERPRETER_wireguard_list0 (arity-specific), both of which
# have higher priority than TERMINAL_INTERPRETER
rm -f "$TEST_DIR/.env/default/VPN_SWITCH_INTERPRETER_wireguard_list" \
"$TEST_DIR/.env/default/VPN_SWITCH_INTERPRETER_wireguard_list0"
rm -f "$TEST_DIR/.env/local/VPN_SWITCH_INTERPRETER_wireguard_list" \
"$TEST_DIR/.env/local/VPN_SWITCH_INTERPRETER_wireguard_list0"
# Pick a single underscore (terminal) function
# _wireguard_list0 is a good choice - it's a terminal function
local output=$(run_vpn_switch wireguard list 2>&1)
# Verify TERMINAL_INTERPRETER was used (output shows it was executed)
if printf '%s\n' "$output" | grep -q "TERMINAL_FIRED"; then
pass "Single underscore function used TERMINAL_INTERPRETER"
else
fail "Single underscore function did not use TERMINAL_INTERPRETER"
printf '%s\n' "Output: $output"
fi
}
# TEST 66: Double underscore → COMBINATOR_INTERPRETER (intrinsic classification)
test_interpreter_double_underscore() {
test_header "interpreter resolution - double underscore uses COMBINATOR_INTERPRETER" "test_interpreter_double_underscore"
test_setup
# Set COMBINATOR_INTERPRETER to distinctive value
# This tests intrinsic classification for double underscore functions
run_vpn_switch setenv "VPN_SWITCH_COMBINATOR_INTERPRETER" "echo COMBINATOR_FIRED"
# IMPORTANT: Remove function-specific overrides that would block intrinsic classification
# The profile may include function-specific interpreter settings
rm -f "$TEST_DIR/.env/default/VPN_SWITCH_INTERPRETER_wireguard_start"
rm -f "$TEST_DIR/.env/local/VPN_SWITCH_INTERPRETER_wireguard_start"
# Pick a double underscore (combinator) function
# __wireguard_start1 is a good choice - it's a combinator that delegates to _wireguard_start1
local output=$(run_vpn_switch wireguard start privacy 2>&1)
# Verify COMBINATOR_INTERPRETER was used
if printf '%s\n' "$output" | grep -q "COMBINATOR_FIRED"; then
pass "Double underscore function used COMBINATOR_INTERPRETER"
else
fail "Double underscore function did not use COMBINATOR_INTERPRETER"
printf '%s\n' "Output: $output"
fi
}
# TEST 67: Triple underscore → BATCH_COMBINATOR_INTERPRETER (intrinsic classification)
test_interpreter_triple_underscore() {
test_header "interpreter resolution - triple underscore uses BATCH_COMBINATOR_INTERPRETER" "test_interpreter_triple_underscore"
test_setup
# Set BATCH_COMBINATOR_INTERPRETER to distinctive value
# This tests intrinsic classification for triple underscore functions
run_vpn_switch setenv "VPN_SWITCH_BATCH_COMBINATOR_INTERPRETER" "echo BATCH_COMBINATOR_FIRED"
# IMPORTANT: Do NOT set function-specific override
# We want to test that triple underscore functions use BATCH_COMBINATOR_INTERPRETER
# Pick a triple underscore (batch combinator) function
# ___dump0 is a good choice - it outputs multiple delegation commands
local output=$(run_vpn_switch dump 2>&1)
# Verify BATCH_COMBINATOR_INTERPRETER was used
if printf '%s\n' "$output" | grep -q "BATCH_COMBINATOR_FIRED"; then
pass "Triple underscore function used BATCH_COMBINATOR_INTERPRETER"
else
fail "Triple underscore function did not use BATCH_COMBINATOR_INTERPRETER"
printf '%s\n' "Output: $output"
fi
}
# TEST 68: Resolution priority - function-specific override beats intrinsic classification
test_interpreter_resolution_priority() {
test_header "interpreter resolution - function-specific override beats intrinsic classification" "test_interpreter_resolution_priority"
test_setup
# Set BOTH function-specific override AND intrinsic classification
# Function-specific should win (upper half beats lower half)
run_vpn_switch setenv "VPN_SWITCH_TERMINAL_INTERPRETER" "echo TERMINAL_DEFAULT"
# Now set function-specific override (should take priority)
set_function_interpreter "wireguard_list" "echo FUNCTION_SPECIFIC_OVERRIDE"
# Call wireguard list (single underscore = terminal function)
local output=$(run_vpn_switch wireguard list 2>&1)
# Verify function-specific override was used (NOT intrinsic classification)
if printf '%s\n' "$output" | grep -q "FUNCTION_SPECIFIC_OVERRIDE"; then
pass "Function-specific override takes priority over intrinsic classification"
else
fail "Function-specific override did not take priority"
printf '%s\n' "Output: $output"
fi
# Verify intrinsic classification was NOT used
if ! printf '%s\n' "$output" | grep -q "TERMINAL_DEFAULT"; then
pass "Intrinsic classification was correctly bypassed"
else
fail "Intrinsic classification was incorrectly used"
printf '%s\n' "Output: $output"
fi
}
#-----------------------------------------------------------------------------
# Protocol-Agnostic Start Tests (Phase 3.2.1)
#-----------------------------------------------------------------------------
test_start_wireguard_only() {
test_header "start - auto-detect WireGuard config" "test_start_wireguard_only"
test_setup
# Create WireGuard config only (must be valid for start to proceed)
mkdir -p "$TEST_DIR/wireguard"
cat > "$TEST_DIR/wireguard/wg-test.conf" <<'EOF'
[Interface]
PrivateKey = test-key
Address = 10.2.0.2/32
EOF
chmod 0400 "$TEST_DIR/wireguard/wg-test.conf"
# Test: should delegate to wireguard start
# We observe the resolution result via the session metadata that
# session create + session populate write to disk before the phase
# scripts run (and possibly fail without root). Stdout no longer
# carries dispatch evidence — _batch2's "# Executing: ..." lines went
# to trace_log in commit fd79dc8 (was needed so `dump` stderr stays clean).
run_vpn_switch start wg-test.conf > /dev/null 2>&1 || true
local sess=$(ls -d "$TEST_DIR"/.session/*/ 2>/dev/null | head -1)
local sess_protocol=""
[ -n "$sess" ] && sess_protocol=$(cat "$sess/protocol" 2>/dev/null)
if [ "$sess_protocol" = "wireguard" ]; then
pass "Delegates to wireguard commands for WireGuard-only config (session.protocol=wireguard)"
else
fail "Did not delegate to wireguard start"
printf '%s\n' " session: ${sess:-none}, session.protocol: ${sess_protocol:-none}"
ls -la "$TEST_DIR/.session/" 2>/dev/null
fi
}
test_start_openvpn_only() {
test_header "start - auto-detect OpenVPN config" "test_start_openvpn_only"
test_setup
# Create OpenVPN config only (must be valid for start to proceed)
mkdir -p "$TEST_DIR/openvpn"
cat > "$TEST_DIR/openvpn/ovpn-test.ovpn" <<'EOF'
client
dev tun
proto udp
remote 1.2.3.4 1194
EOF
chmod 0400 "$TEST_DIR/openvpn/ovpn-test.ovpn"
# Test: should delegate to openvpn start
# Twin of test_start_wireguard_only — assert on session.protocol
# artifact rather than _batch2 stdout, which moved to trace_log in fd79dc8.
run_vpn_switch start ovpn-test.ovpn > /dev/null 2>&1 || true
local sess=$(ls -d "$TEST_DIR"/.session/*/ 2>/dev/null | head -1)
local sess_protocol=""
[ -n "$sess" ] && sess_protocol=$(cat "$sess/protocol" 2>/dev/null)
if [ "$sess_protocol" = "openvpn" ]; then
pass "Delegates to openvpn commands for OpenVPN-only config (session.protocol=openvpn)"
else
fail "Did not delegate to openvpn start"
printf '%s\n' " session: ${sess:-none}, session.protocol: ${sess_protocol:-none}"
ls -la "$TEST_DIR/.session/" 2>/dev/null
fi
}
test_start_not_found() {
test_header "start - error on nonexistent config" "test_start_not_found"
test_setup
# Test: should error with helpful message
local output=$(run_vpn_switch start nonexistent 2>&1)
if printf '%s\n' "$output" | grep -q "config not found: nonexistent"; then
pass "Error message for nonexistent config"
else
fail "Did not show proper error message"
printf '%s\n' "Output: $output"
fi
# Verify it searched both directories
if printf '%s\n' "$output" | grep -q "wireguard/nonexistent" && printf '%s\n' "$output" | grep -q "openvpn/nonexistent"; then
pass "Error shows both search locations"
else
fail "Error does not show search locations"
printf '%s\n' "Output: $output"
fi
}
test_start_no_default() {
test_header "start - error when no configs exist (Phase 3.4.1)" "test_start_no_default"
test_setup
# Remove all configs to test empty database case
rm -f "$TEST_DIR/wireguard"/*.conf 2>/dev/null || true
rm -f "$TEST_DIR/openvpn"/*.ovpn 2>/dev/null || true
# Test: should error when no VPN configs are available
local output=$(run_vpn_switch start 2>&1)
if printf '%s\n' "$output" | grep -q "no VPN protocols found"; then
pass "Error message when no configs available"
else
fail "Did not show proper error message"
printf '%s\n' "Output: $output"
fi
# Verify helpful hint is provided
if printf '%s\n' "$output" | grep -q "vpn-switch import"; then
pass "Error provides hint to import configs"
else
fail "Error does not provide helpful hint"
printf '%s\n' "Output: $output"
fi
}
test_start_with_default_wireguard() {
test_header "start - use default link (WireGuard) via API" "test_start_with_default_wireguard"
test_setup
# Create WireGuard config using proper API
local test_config="$TEST_BASE_DIR/wg-test-default.conf"
rm -f "$test_config"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test-key-default-wg
Address = 10.2.0.3/32
EOF
chmod 0400 "$test_config"
# Import config through API
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Override interpreter to prevent actual connection
override_for_command_inspection "wireguard_connect"
# Start session (creates .session/<PID>)
local start_output
start_output=$(run_vpn_switch wireguard start wg-test-default 2>&1)
# Extract PID from output
local session_pid
session_pid=$(echo "$start_output" | grep -o 'session [0-9]*' | awk '{print $2}')
if [ -z "$session_pid" ]; then
fail "Could not create session for default link test"
return
fi
# Save as default using API
VPN_SWITCH_BASE="$TEST_DIR" \
VPN_SWITCH_SESSION_PID="$session_pid" \
run_vpn_switch session save >/dev/null 2>&1
# Verify default link was created
if [ -L "$TEST_DIR/session/default" ]; then
pass "Default session link created via API"
else
fail "Default session link not created"
return
fi
# Now test: start with no args should delegate to session start
# Override interpreter to inspect delegation (not execute)
override_for_command_inspection "start"
local output
output=$(run_vpn_switch start 2>&1)
if printf '%s\n' "$output" | grep -q 'session start'; then
pass "Start with no args resolves to default session"
else
fail "Default link did not resolve correctly"
printf '%s\n' "Output: $output"
fi
}
test_start_with_default_openvpn() {
test_header "start - use default link (OpenVPN) via API" "test_start_with_default_openvpn"
test_setup
# Create OpenVPN config using proper API
local test_config="$TEST_BASE_DIR/ovpn-test-default.ovpn"
rm -f "$test_config"
cat > "$test_config" <<'EOF'
client
dev tun
proto udp
remote 5.6.7.8 1194
EOF
chmod 0400 "$test_config"
# Import config through API
run_vpn_switch openvpn import "$test_config" >/dev/null 2>&1
# Override interpreter to prevent actual connection
override_for_command_inspection "openvpn_connect"
# Start session (creates .session/<PID>)
local start_output
start_output=$(run_vpn_switch openvpn start ovpn-test-default 2>&1)
# Extract PID from output
local session_pid
session_pid=$(echo "$start_output" | grep -o 'session [0-9]*' | awk '{print $2}')
if [ -z "$session_pid" ]; then
fail "Could not create session for default link test"
return
fi
# Save as default using API
VPN_SWITCH_BASE="$TEST_DIR" \
VPN_SWITCH_SESSION_PID="$session_pid" \
run_vpn_switch session save >/dev/null 2>&1
# Verify default link was created
if [ -L "$TEST_DIR/session/default" ]; then
pass "Default session link created via API"
else
fail "Default session link not created"
return
fi
# Now test: start with no args should delegate to session start
# Override interpreter to inspect delegation (not execute)
override_for_command_inspection "start"
local output
output=$(run_vpn_switch start 2>&1)
if printf '%s\n' "$output" | grep -q 'session start'; then
pass "Start with no args resolves to default session"
else
fail "Default link did not resolve correctly"
printf '%s\n' "Output: $output"
fi
}
test_start_default_invalid_target() {
test_header "start - error on invalid default link target" "test_start_default_invalid_target"
test_setup
# Create WireGuard config using proper API
local test_config="$TEST_BASE_DIR/wg-test-invalid.conf"
rm -f "$test_config"
cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = test-key-invalid
Address = 10.2.0.4/32
EOF
chmod 0400 "$test_config"
# Import config through API
run_vpn_switch wireguard import "$test_config" >/dev/null 2>&1
# Override interpreter to prevent actual connection
override_for_command_inspection "wireguard_connect"
# Start session and save as default
local start_output
start_output=$(run_vpn_switch wireguard start wg-test-invalid 2>&1)
local session_pid
session_pid=$(echo "$start_output" | grep -o 'session [0-9]*' | awk '{print $2}')
if [ -n "$session_pid" ]; then
VPN_SWITCH_BASE="$TEST_DIR" \
VPN_SWITCH_SESSION_PID="$session_pid" \
run_vpn_switch session save >/dev/null 2>&1
fi
# Now manually corrupt the default link to test error handling
# (This simulates manual filesystem corruption/manipulation)
rm -f "$TEST_DIR/session/default"
ln -s "../invalid/config.conf" "$TEST_DIR/session/default"
# Test: should error with helpful message
local output
output=$(run_vpn_switch start 2>&1)
if printf '%s\n' "$output" | grep -qE "Error|not found|invalid"; then
pass "Error detected for invalid default session target"
else
fail "Did not detect invalid default session target"
printf '%s\n' "Output: $output"
fi
}
test_init_extension_links() {
test_header "init - creates protocol extension links (Phase 3.4.1)" "test_init_extension_links"
test_setup
# Run init
run_vpn_switch init minimal >/dev/null 2>&1
# Verify extension links exist
if [ -L "$TEST_DIR/conf" ]; then
pass "conf link created"
else
fail "conf link not created"
fi
if [ -L "$TEST_DIR/ovpn" ]; then
pass "ovpn link created"
else
fail "ovpn link not created"
fi
# Verify targets
local conf_target=$(readlink "$TEST_DIR/conf")
if [ "$conf_target" = "wireguard" ]; then
pass "conf → wireguard"
else
fail "conf link has wrong target: $conf_target"
fi
local ovpn_target=$(readlink "$TEST_DIR/ovpn")
if [ "$ovpn_target" = "openvpn" ]; then
pass "ovpn → openvpn"
else
fail "ovpn link has wrong target: $ovpn_target"
fi
}
# Phase 4.4: Init function tests
test_database_init() {
test_header "database init - creates directory structure (Phase 4.4)" "test_database_init"
test_setup
# database_init0 creates all required directories
run_vpn_switch init minimal >/dev/null 2>&1
# Verify core directories
[ -d "$TEST_DIR/wireguard" ] && pass "wireguard/ created" || fail "wireguard/ missing"
[ -d "$TEST_DIR/openvpn" ] && pass "openvpn/ created" || fail "openvpn/ missing"
[ -d "$TEST_DIR/.session" ] && pass ".session/ created" || fail ".session/ missing"
[ -d "$TEST_DIR/session" ] && pass "session/ created" || fail "session/ missing"
[ -d "$TEST_DIR/.env/default" ] && pass ".env/default/ created" || fail ".env/default/ missing"
[ -d "$TEST_DIR/.env/local" ] && pass ".env/local/ created" || fail ".env/local/ missing"
[ -d "$TEST_DIR/.include/phase" ] && pass ".include/phase/ created" || fail ".include/phase/ missing"
chmod 0755 "$TEST_DIR/openpgp"
local init_out=$(run_vpn_switch database init 2>&1)
if [ "$($EXAMINE_FILE_PERMS "$TEST_DIR/openpgp" 2>/dev/null | sed 's/^0*//')" = "700" ] && printf '%s\n' "$init_out" | grep -q "Fixed: Directory openpgp"; then
pass "a directory of the layout with the wrong mode is set right by init"
else
fail "init left openpgp at $($EXAMINE_FILE_PERMS "$TEST_DIR/openpgp" 2>/dev/null): $(printf '%s\n' "$init_out" | grep -i "openpgp" | head -2 | tr '\n' ' ')"
fi
}
test_environment_init() {
test_header "environment init - copies profile templates (Phase 4.4)" "test_environment_init"
test_setup
run_vpn_switch init minimal >/dev/null 2>&1
# Check profile templates were copied (uppercase profile name)
if [ -f "$TEST_DIR/.env/default/VPN_SWITCH_PROFILE_MINIMAL" ]; then
pass "minimal profile copied"
else
fail "minimal profile not copied"
fi
# Verify permissions (use helper function for cross-platform)
local perms=$(get_file_perms_full "$TEST_DIR/.env/default/VPN_SWITCH_PROFILE_MINIMAL")
if [ "$perms" = "100600" ] || [ "$perms" = "600" ]; then
pass "profile has correct permissions (0600)"
else
fail "profile has wrong permissions: $perms"
fi
}
test_environment_init_platform_template() {
test_header "environment init - a platform variant of a template wins over the generic one" "test_environment_init_platform_template"
test_setup
local tpl="$(dirname "$TEST_SCRIPT")/template" os installed
os=$(readlink "$tpl/platform/default"); os=${os##*/}; os=${os%.sh}
run_vpn_switch environment init minimal >/dev/null 2>&1
installed=$(head -n1 "$TEST_DIR/.env/default/VPN_SWITCH_ARCHIVER" 2>/dev/null)
if [ -f "$tpl/platform/$os/environment/VPN_SWITCH_ARCHIVER" ]; then
if [ "$installed" = "$(head -n1 "$tpl/platform/$os/environment/VPN_SWITCH_ARCHIVER")" ]; then
pass "VPN_SWITCH_ARCHIVER installed from template/platform/$os/environment"
else
fail "VPN_SWITCH_ARCHIVER not the $os variant: $installed"
fi
else
if [ "$installed" = "$(head -n1 "$tpl/environment/VPN_SWITCH_ARCHIVER")" ]; then
pass "VPN_SWITCH_ARCHIVER installed from template/environment ($os has no variant)"
else
fail "VPN_SWITCH_ARCHIVER not the generic template: $installed"
fi
fi
if [ "$(head -n1 "$TEST_DIR/.env/default/VPN_SWITCH_EXTRACTOR" 2>/dev/null)" = "$(head -n1 "$tpl/environment/VPN_SWITCH_EXTRACTOR")" ]; then
pass "VPN_SWITCH_EXTRACTOR installed from template/environment (no platform variant)"
else
fail "VPN_SWITCH_EXTRACTOR wrong: $(head -n1 "$TEST_DIR/.env/default/VPN_SWITCH_EXTRACTOR" 2>&1)"
fi
}
test_phases_copy() {
test_header "phases copy - copies phase templates (Phase 4.4)" "test_phases_copy"
test_setup
run_vpn_switch init minimal >/dev/null 2>&1
# Verify phase templates were copied
[ -d "$TEST_DIR/.include/phase/wireguard" ] && pass "wireguard phases copied" || fail "wireguard phases missing"
[ -d "$TEST_DIR/.include/phase/openvpn" ] && pass "openvpn phases copied" || fail "openvpn phases missing"
# Verify phase directories have content
local wg_count=$(find "$TEST_DIR/.include/phase/wireguard" -type d | wc -l)
[ "$wg_count" -gt 1 ] && pass "wireguard phase subdirs exist" || fail "wireguard phases empty"
}
test_phases_lift() {
test_header "phases lift - lifts phase:backend templates (Phase 4.4)" "test_phases_lift"
test_setup
run_vpn_switch init minimal >/dev/null 2>&1
# phases lift generates commands to lift a specific backend
local output=$(run_vpn_switch phases lift firewall:pf 2>&1)
# Should generate lift commands (check for completion message)
printf '%s\n' "$output" | grep -q "Lifted: firewall" && pass "generates lift command" || fail "no lift command"
# Check that the lifted file was created and is executable
local lifted_file="$TEST_DIR/.include/phase/base/firewall/pf.sh"
[ -x "$lifted_file" ] && pass "output file is executable" || fail "output file not executable or missing"
}
test_phases_default_profile() {
test_header "phases - default profile symlink present + validate flags its absence" "test_phases_default_profile"
test_setup
run_vpn_switch init minimal >/dev/null 2>&1
# The default phase-profile symlink is load-bearing: phase specs without an
# explicit @profile (and any :variant) resolve via .include/phase/default -> base.
[ -L "$TEST_DIR/.include/phase/default" ] && pass ".include/phase/default symlink present" || fail ".include/phase/default symlink missing"
[ -d "$TEST_DIR/.include/phase/default" ] && pass "default profile resolves to a directory" || fail "default profile does not resolve"
# DB health: validate must FLAG a missing default symlink (not pass silently)
rm -f "$TEST_DIR/.include/phase/default"
local output=$(run_vpn_switch phases validate 2>&1 || true)
if printf '%s\n' "$output" | grep -qi "default" && printf '%s\n' "$output" | grep -qiE "ERROR|missing|✗"; then
pass "validate flags missing default symlink"
else
fail "validate did not flag missing default symlink"
fi
}
test_phases_sync_single() {
test_header "phases sync <phase> - discovers and lifts backends (Phase 4.4)" "test_phases_sync_single"
test_setup
run_vpn_switch init minimal >/dev/null 2>&1
# Delegation evidence is in the trace file now (fd79dc8 moved batch
# dispatch messages off stdout). Capture trace_file specifically for
# this run and assert on it. export is necessary because run_vpn_switch
# is a shell function whose inner subshell needs the variable in env.
local trace_file=$(mktemp)
export VPN_SWITCH_TRACE_FILE="$trace_file"
run_vpn_switch phases sync firewall >/dev/null 2>&1 || true
unset VPN_SWITCH_TRACE_FILE
if grep -q 'phases lift' "$trace_file"; then
pass "delegates to phases lift"
else
fail "no delegation"
echo " trace file: $trace_file"
head -20 "$trace_file" 2>/dev/null
fi
rm -f "$trace_file"
}
test_phases_sync_all() {
test_header "phases sync - syncs all phases (Phase 4.4)" "test_phases_sync_all"
test_setup
run_vpn_switch init minimal >/dev/null 2>&1
# Same pattern as test_phases_sync_single — trace-file based assertion.
local trace_file=$(mktemp)
export VPN_SWITCH_TRACE_FILE="$trace_file"
run_vpn_switch phases sync >/dev/null 2>&1 || true
unset VPN_SWITCH_TRACE_FILE
if grep -q 'phases sync' "$trace_file"; then
pass "delegates to single-phase sync"
else
fail "no delegation"
echo " trace file: $trace_file"
head -20 "$trace_file" 2>/dev/null
fi
rm -f "$trace_file"
local vpn_out=$(run_vpn_switch phases sync vpn 2>&1)
if printf '%s\n' "$vpn_out" | grep -q "^# phase vpn: no base backends" && ! printf '%s\n' "$vpn_out" | grep -qi "warning"; then
pass "the vpn phase is a note, not a warning (the protocol templates provide it)"
else
fail "vpn phase: $(printf '%s\n' "$vpn_out" | head -2 | tr '\n' ' ')"
fi
}
test_init_batch_combinator() {
test_header "init - orchestrates full initialization (Phase 4.4)" "test_init_batch_combinator"
test_setup
# init should call database init, environment init, and phases sync
local output=$(run_vpn_switch init minimal 2>&1)
# Check for success indicators (database may be validated if already exists)
if printf '%s\n' "$output" | grep -qE "Created directory|Validated:"; then
pass "database init executed"
else
fail "no database init"
fi
printf '%s\n' "$output" | grep -q "Installed.*profile" && pass "environment init executed" || fail "no env init"
printf '%s\n' "$output" | grep -q "Copied phase templates" && pass "phases copy executed" || fail "no phases copy"
}
test_init_combinator() {
test_header "init combinator - delegates to batch combinator (Phase 4.4)" "test_init_combinator"
test_setup
# __init0 should delegate to ___init1
local output=$(run_vpn_switch init minimal 2>&1)
# Should result in functional database
[ -d "$TEST_DIR/wireguard" ] && pass "database created" || fail "database not created"
[ -f "$TEST_DIR/.env/default/VPN_SWITCH_PROFILE_MINIMAL" ] && pass "profile installed" || fail "profile missing"
}
test_start_extension_link_fast_path() {
test_header "start - uses extension link for protocol resolution (Phase 3.4.1)" "test_start_extension_link_fast_path"
test_setup
# Create WireGuard config
cat > "$TEST_DIR/wireguard/test.conf" <<'EOF'
[Interface]
PrivateKey = test
Address = 10.2.0.2/32
EOF
chmod 0400 "$TEST_DIR/wireguard/test.conf"
# Verify extension link exists (from init)
if [ ! -L "$TEST_DIR/conf" ]; then
fail "Extension link conf not found"
return 1
fi
# Test: start with .conf extension should use fast path
# As in test_start_wireguard_only: assert on the durable artifact, not
# _batch2's stdout chatter which moved to trace_log in fd79dc8.
run_vpn_switch start test.conf > /dev/null 2>&1 || true
local sess=$(ls -d "$TEST_DIR"/.session/*/ 2>/dev/null | head -1)
local sess_protocol=""
[ -n "$sess" ] && sess_protocol=$(cat "$sess/protocol" 2>/dev/null)
if [ "$sess_protocol" = "wireguard" ]; then
pass "Extension link used for fast protocol resolution (session.protocol=wireguard)"
else
fail "Extension link not used for resolution"
printf '%s\n' " session: ${sess:-none}, session.protocol: ${sess_protocol:-none}"
ls -la "$TEST_DIR/.session/" 2>/dev/null
fi
}
test_start_multiple_matches() {
test_header "start - error on multiple protocol matches (Phase 3.4.1)" "test_start_multiple_matches"
test_setup
# Create same basename in both protocols
cat > "$TEST_DIR/wireguard/server.conf" <<'EOF'
[Interface]
PrivateKey = test1
Address = 10.2.0.2/32
EOF
chmod 0400 "$TEST_DIR/wireguard/server.conf"
cat > "$TEST_DIR/openvpn/server.ovpn" <<'EOF'
client
remote 1.2.3.4 1194
EOF
chmod 0400 "$TEST_DIR/openvpn/server.ovpn"
# Test: start with ambiguous name
local output=$(run_vpn_switch start server 2>&1)
# Should show multiple matches error
if printf '%s\n' "$output" | grep -q "Multiple configs match"; then
pass "Multiple matches error shown"
else
fail "Multiple matches error not shown"
printf '%s\n' "Output: $output"
fi
# Should list both matches
if printf '%s\n' "$output" | grep -q "wireguard/server.conf" && printf '%s\n' "$output" | grep -q "openvpn/server.ovpn"; then
pass "Both matches listed"
else
fail "Not all matches shown"
printf '%s\n' "Output: $output"
fi
}
#-----------------------------------------------------------------------------
# OpenVPN Config Management Tests (Phase 3.1.4)
#-----------------------------------------------------------------------------
test_openvpn_import_valid() {
test_header "openvpn import - import valid .ovpn config" "test_openvpn_import_valid"
test_setup
# Enable execution for openvpn import
set_function_interpreter "openvpn_import1" "sh"
# Create test .ovpn file
local test_config="$TEST_BASE_DIR/test-us-server.ovpn"
cat > "$test_config" <<'EOF'
client
dev tun
proto udp
remote 1.2.3.4 1194
resolv-retry infinite
nobind
persist-key
persist-tun
cipher AES-256-CBC
auth SHA256
verb 3
EOF
chmod 0600 "$test_config"
# Run import
run_vpn_switch openvpn import "$test_config" >/dev/null 2>&1
# Verify imported
local imported="$TEST_DIR/openvpn/test-us-server.ovpn"
if [ -f "$imported" ]; then
pass "Config imported to openvpn/"
else
fail "Config not imported"
fi
# Verify permissions
if [ -f "$imported" ]; then
local perms=$(get_file_perms "$imported")
if [ "$perms" = "400" ]; then
pass "Permissions set to 0400"
else
fail "Permissions incorrect: got $perms, expected 400"
fi
fi
rm -f "$test_config"
}
test_openvpn_list() {
test_header "openvpn list - list configs and groups" "test_openvpn_list"
test_setup
# Create test files manually
mkdir -p "$TEST_DIR/openvpn/work"
touch "$TEST_DIR/openvpn/us-server.ovpn"
touch "$TEST_DIR/openvpn/eu-server.ovpn"
ln -s "./us-server.ovpn" "$TEST_DIR/openvpn/work/office"
# Run list command
local output=$(run_vpn_switch openvpn list 2>/dev/null)
# Check for configs
if printf '%s\n' "$output" | grep -qx "openvpn/us-server.ovpn"; then
pass "Lists config: us-server.ovpn"
else
fail "Missing config in list"
fi
# Check for group
if printf '%s\n' "$output" | grep -qx "openvpn/work/"; then
pass "Lists group: work/"
else
fail "Missing group in list"
fi
# Check arity-1 (list specific group)
local group_output=$(run_vpn_switch openvpn list work 2>/dev/null)
if echo "$group_output" | grep -qx "openvpn/work/office -> ./us-server.ovpn"; then
pass "Lists group link with arity-1: work/office"
else
fail "Missing group link in arity-1 list"
fi
}
test_openvpn_add_group() {
test_header "openvpn add - create group" "test_openvpn_add_group"
test_setup
# Enable execution
set_function_interpreter "openvpn_add" "sh"
# Run command
run_vpn_switch openvpn add work >/dev/null 2>&1
# Verify group created
if [ -d "$TEST_DIR/openvpn/work" ]; then
pass "Group directory created"
else
fail "Group directory not created"
fi
# Verify permissions
local perms=$(get_file_perms "$TEST_DIR/openvpn/work")
if [ "$perms" = "700" ]; then
pass "Group permissions correct (0700)"
else
fail "Group permissions incorrect: $perms"
fi
}
test_openvpn_add_to_group_arity2() {
test_header "openvpn add - add config to group (arity-2)" "test_openvpn_add_group"
test_setup
# Setup
mkdir -p "$TEST_DIR/openvpn/work"
touch "$TEST_DIR/openvpn/us-server.ovpn"
# Enable execution
set_function_interpreter "openvpn_add" "sh"
# Run command (arity-2: link name = config name minus extension)
run_vpn_switch openvpn add work us-server.ovpn >/dev/null 2>&1
# Verify link created
if [ -L "$TEST_DIR/openvpn/work/us-server" ]; then
pass "Link created: work/us-server"
else
fail "Link not created"
fi
# Verify link target
local target=$(readlink "$TEST_DIR/openvpn/work/us-server")
if [ "$target" = "../us-server.ovpn" ]; then
pass "Link points to correct target"
else
fail "Link target incorrect: $target"
fi
}
test_openvpn_add_to_group_arity3() {
test_header "openvpn add - add with custom alias (arity-3)" "test_openvpn_add_group"
test_setup
mkdir -p "$TEST_DIR/openvpn/work"
touch "$TEST_DIR/openvpn/us-server.ovpn"
set_function_interpreter "openvpn_add" "sh"
# Run with custom alias
run_vpn_switch openvpn add work us-server.ovpn office >/dev/null 2>&1
# Verify custom link created
if [ -L "$TEST_DIR/openvpn/work/office" ]; then
pass "Custom alias created: work/office"
else
fail "Custom alias not created"
fi
# Verify target
local target=$(readlink "$TEST_DIR/openvpn/work/office")
if [ "$target" = "../us-server.ovpn" ]; then
pass "Alias points to correct target"
else
fail "Alias target incorrect: $target"
fi
}
test_openvpn_link() {
test_header "openvpn link - create protocol-level link" "test_openvpn_link"
test_setup
touch "$TEST_DIR/openvpn/us-server.ovpn"
set_function_interpreter "openvpn_link2" "sh"
# Create protocol-level link
run_vpn_switch openvpn link quick us-server.ovpn >/dev/null 2>&1
# Verify link
if [ -L "$TEST_DIR/openvpn/quick" ]; then
pass "Protocol-level link created"
else
fail "Link not created"
fi
# Verify target
local target=$(readlink "$TEST_DIR/openvpn/quick")
if [ "$target" = "./us-server.ovpn" ]; then
pass "Link target correct"
else
fail "Link target incorrect: $target"
fi
}
test_openvpn_remove() {
test_header "openvpn remove - remove config safely" "test_openvpn_remove"
test_setup
touch "$TEST_DIR/openvpn/us-server.ovpn"
touch "$TEST_DIR/openvpn/eu-server.ovpn"
set_function_interpreter "openvpn_remove" "sh"
# Remove config
run_vpn_switch openvpn remove eu-server.ovpn >/dev/null 2>&1
# Verify removed
if [ ! -f "$TEST_DIR/openvpn/eu-server.ovpn" ]; then
pass "Config removed"
else
fail "Config not removed"
fi
# Verify other config still exists
if [ -f "$TEST_DIR/openvpn/us-server.ovpn" ]; then
pass "Other config preserved"
else
fail "Other config incorrectly removed"
fi
}
test_openvpn_validate() {
test_header "openvpn validate - detect broken links" "test_openvpn_validate"
test_setup
# Create broken link
mkdir -p "$TEST_DIR/openvpn/work"
ln -s "../nonexistent.ovpn" "$TEST_DIR/openvpn/work/broken"
# Run validate
local output=$(run_vpn_switch openvpn validate 2>/dev/null)
# Check for broken link detection
if printf '%s\n' "$output" | grep -q "Broken symlink.*work/broken"; then
pass "Broken symlink detected"
else
fail "Broken symlink not detected"
fi
# Remove broken link and create valid setup
run_vpn_switch openvpn remove work/broken >/dev/null 2>&1
cat > "$TEST_DIR/openvpn/server.ovpn" <<'EOF'
client
remote test.example.com 1194
EOF
chmod 0400 "$TEST_DIR/openvpn/server.ovpn"
run_vpn_switch openvpn add work link server.ovpn >/dev/null 2>&1
output=$(run_vpn_switch openvpn validate 2>/dev/null)
if printf '%s\n' "$output" | grep -q "No issues found"; then
pass "Valid database passes validation"
else
fail "Valid database reported issues"
fi
}
test_openvpn_info() {
test_header "openvpn info - show group information" "test_openvpn_info"
test_setup
mkdir -p "$TEST_DIR/openvpn/work"
touch "$TEST_DIR/openvpn/us-server.ovpn"
touch "$TEST_DIR/openvpn/eu-server.ovpn"
ln -s "../us-server.ovpn" "$TEST_DIR/openvpn/work/office"
ln -s "../eu-server.ovpn" "$TEST_DIR/openvpn/work/remote"
# Run info
local output=$(run_vpn_switch openvpn info work 2>/dev/null)
# Check for group name
if printf '%s\n' "$output" | grep -q "Group: work"; then
pass "Shows group name"
else
fail "Missing group name"
fi
# Check for link count
if printf '%s\n' "$output" | grep -q "Total links: 2"; then
pass "Shows correct link count"
else
fail "Incorrect link count"
fi
# Check for links
if printf '%s\n' "$output" | grep -q "office -> ../us-server.ovpn"; then
pass "Shows link: office"
else
fail "Missing link in output"
fi
}
test_openvpn_dump() {
test_header "openvpn dump - export database" "test_openvpn_dump"
test_setup
# Create test database
mkdir -p "$TEST_DIR/openvpn/work"
touch "$TEST_DIR/openvpn/us-server.ovpn"
ln -s "./us-server.ovpn" "$TEST_DIR/openvpn/quick"
ln -s "../us-server.ovpn" "$TEST_DIR/openvpn/work/office"
# Run dump
local output=$(run_vpn_switch openvpn dump 2>/dev/null)
# Check for import command
if printf '%s\n' "$output" | grep -q "openvpn import.*us-server.ovpn"; then
pass "Dump includes import command"
else
fail "Missing import command"
fi
# Check for group creation
if printf '%s\n' "$output" | grep -q "openvpn add work"; then
pass "Dump includes group creation"
else
fail "Missing group creation"
fi
# Check for protocol link
if printf '%s\n' "$output" | grep -q "openvpn link quick"; then
pass "Dump includes protocol link"
else
fail "Missing protocol link"
fi
# Check for group link
if printf '%s\n' "$output" | grep -q "openvpn add work.*us-server.ovpn"; then
pass "Dump includes group link"
else
fail "Missing group link"
fi
}
#-----------------------------------------------------------------------------
# OpenVPN Lifecycle Tests (Phase 3.3.1)
#-----------------------------------------------------------------------------
# TEST: openvpn patch - displays patched configuration
test_openvpn_patch_valid() {
test_header "openvpn patch - displays patched configuration" "test_openvpn_patch_valid"
test_setup
override_for_command_inspection "openvpn_patch"
# Enable execution for openvpn import (narrow override)
set_function_interpreter "openvpn_import1" "sh"
# Create a test OpenVPN config
local test_config="/tmp/us-TEST-PATCH-1.ovpn"
cat > "$test_config" <<'CONF'
client
dev tun
proto udp
remote vpn.example.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
cipher AES-256-CBC
auth SHA256
verb 3
CONF
chmod 0400 "$test_config"
# Import config first
run_vpn_switch openvpn import "$test_config" >/dev/null 2>&1
# Run patch command
local output=$(run_vpn_switch openvpn patch us-TEST-PATCH-1.ovpn 2>&1)
# Phase-based approach: DNS handled by DNS phase, not up/down scripts
# Verify up/down scripts are NOT present (they should be stripped)
if printf '%s\n' "$output" | grep -q "^up "; then
fail "Patch output should NOT contain up script (phase-based DNS)"
else
pass "Patch output correctly strips up script (phase-based DNS)"
fi
if printf '%s\n' "$output" | grep -q "^down "; then
fail "Patch output should NOT contain down script (phase-based DNS)"
else
pass "Patch output correctly strips down script (phase-based DNS)"
fi
# Verify output contains daemon mode
if printf '%s\n' "$output" | grep -q "daemon"; then
pass "Patch output contains daemon mode"
else
fail "Patch output missing daemon mode"
fi
# Verify output contains device interface
if printf '%s\n' "$output" | grep -q "dev tun"; then
pass "Patch output contains device interface"
else
fail "Patch output missing device interface"
fi
# Verify output contains writepid
if printf '%s\n' "$output" | grep -q "writepid"; then
pass "Patch output contains writepid directive"
else
fail "Patch output missing writepid"
fi
# Verify output still contains original config elements
if printf '%s\n' "$output" | grep -q "client"; then
pass "Patch output preserves client directive"
else
fail "Patch output lost client directive"
fi
if printf '%s\n' "$output" | grep -q "remote vpn.example.com"; then
pass "Patch output preserves remote server"
else
fail "Patch output lost remote server"
fi
# Cleanup
rm -f "$test_config"
}
# TEST: openvpn patch - handles nonexistent config
test_openvpn_patch_nonexistent() {
test_header "openvpn patch - handles nonexistent config" "test_openvpn_patch_nonexistent"
test_setup
override_for_command_inspection "openvpn_patch"
# Run patch with nonexistent config
local output=$(run_vpn_switch openvpn patch nonexistent.ovpn 2>&1 || true)
# Should get an error
if printf '%s\n' "$output" | grep -qiE "not found|error"; then
pass "Error message for nonexistent config"
else
fail "Missing error for nonexistent config"
fi
}
# TEST: openvpn patch - handles absolute path
test_openvpn_patch_absolute() {
test_header "openvpn patch - handles absolute path" "test_openvpn_patch_absolute"
test_setup
override_for_command_inspection "openvpn_patch"
# Create config with absolute path
local test_config="/tmp/us-absolute.ovpn"
cat > "$test_config" <<'CONF'
client
dev tun
remote 1.2.3.4 1194
CONF
chmod 0400 "$test_config"
# Patch with absolute path (no import needed)
local output=$(run_vpn_switch openvpn patch "$test_config" 2>&1)
# Should work and contain patched elements
if printf '%s\n' "$output" | grep -q "daemon"; then
pass "Absolute path patch works"
else
fail "Absolute path patch failed"
fi
# Cleanup
rm -f "$test_config"
}
# TEST: openvpn patch - patches bare auth-user-pass with credentials
test_openvpn_patch_credentials_bare() {
test_header "openvpn patch - patches bare auth-user-pass with credentials" "test_openvpn_patch_credentials_bare"
test_setup
override_for_command_inspection "openvpn_patch"
# Enable execution for openvpn import
set_function_interpreter "openvpn_import1" "sh"
# Create test config with bare auth-user-pass
local test_config="/tmp/us-creds-bare.ovpn"
rm -f "$test_config"
cat > "$test_config" <<'CONF'
client
dev tun
remote vpn.example.com 1194
auth-user-pass
CONF
chmod 0400 "$test_config"
# Import config
run_vpn_switch openvpn import "$test_config" >/dev/null 2>&1
# Test WITH credentials environment variable (use setenv - proper way)
run_vpn_switch setenv VPN_SWITCH_CREDENTIALS_openvpn "/path/to/credentials.txt" >/dev/null 2>&1
local output=$(run_vpn_switch openvpn patch us-creds-bare.ovpn 2>&1)
if printf '%s\n' "$output" | grep -q "auth-user-pass /path/to/credentials.txt"; then
pass "Bare auth-user-pass patched with credentials path"
else
fail "Bare auth-user-pass not patched with credentials"
fi
# Test WITHOUT credentials environment variable (unset it first)
run_vpn_switch unsetenv VPN_SWITCH_CREDENTIALS_openvpn >/dev/null 2>&1
output=$(run_vpn_switch openvpn patch us-creds-bare.ovpn 2>&1)
if printf '%s\n' "$output" | grep -q "^auth-user-pass$"; then
pass "Bare auth-user-pass preserved when no credentials set"
else
fail "Bare auth-user-pass should remain unchanged without credentials var"
fi
# Cleanup
rm -f "$test_config"
}
# TEST: openvpn patch - preserves existing auth-user-pass path
test_openvpn_patch_credentials_existing() {
test_header "openvpn patch - preserves existing auth-user-pass path" "test_openvpn_patch_credentials_existing"
test_setup
override_for_command_inspection "openvpn_patch"
# Enable execution for openvpn import
set_function_interpreter "openvpn_import1" "sh"
# Create test config with existing auth-user-pass path
local test_config="/tmp/us-creds-existing.ovpn"
cat > "$test_config" <<'CONF'
client
dev tun
remote vpn.example.com 1194
auth-user-pass /existing/path/credentials.txt
CONF
chmod 0400 "$test_config"
# Import config
run_vpn_switch openvpn import "$test_config" >/dev/null 2>&1
# Test with different credentials path - should NOT override
local output=$(VPN_SWITCH_CREDENTIALS_openvpn="/different/credentials.txt" \
run_vpn_switch openvpn patch us-creds-existing.ovpn 2>&1)
if printf '%s\n' "$output" | grep -q "auth-user-pass /existing/path/credentials.txt"; then
pass "Existing auth-user-pass path preserved (not overridden)"
else
fail "Existing auth-user-pass path should not be modified"
fi
# Cleanup
rm -f "$test_config"
}
# TEST: openvpn patch - handles config without auth-user-pass
test_openvpn_patch_credentials_missing() {
test_header "openvpn patch - handles config without auth-user-pass" "test_openvpn_patch_credentials_missing"
test_setup
override_for_command_inspection "openvpn_patch"
# Enable execution for openvpn import
set_function_interpreter "openvpn_import1" "sh"
# Create test config without auth-user-pass
local test_config="/tmp/us-no-auth.ovpn"
cat > "$test_config" <<'CONF'
client
dev tun
remote vpn.example.com 1194
CONF
chmod 0400 "$test_config"
# Import config
run_vpn_switch openvpn import "$test_config" >/dev/null 2>&1
# Test with credentials set - should not add auth-user-pass
local output=$(VPN_SWITCH_CREDENTIALS_openvpn="/path/to/credentials.txt" \
run_vpn_switch openvpn patch us-no-auth.ovpn 2>&1)
if printf '%s\n' "$output" | grep -vq "auth-user-pass"; then
pass "Config without auth-user-pass remains unchanged"
else
fail "Should not add auth-user-pass to configs that don't have it"
fi
# Cleanup
rm -f "$test_config"
}
# TEST: openvpn start - create session and connect
test_openvpn_start_basic() {
test_header "openvpn start - create session and connect to VPN" "test_openvpn_start_basic"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations with unique test ID
local test_id=80
setup_session_test_mocks "$test_id"
local test_interface="tun_test${test_id}"
# Enable execution for openvpn add and import (narrow overrides)
set_function_interpreter "openvpn_add" "sh"
set_function_interpreter "openvpn_import1" "sh"
# Create work category
run_vpn_switch openvpn add work >/dev/null 2>&1
# Import an OpenVPN config
local test_config="$TEST_BASE_DIR/us-server.ovpn"
cat > "$test_config" <<'EOF'
client
dev tun
proto udp
remote vpn.example.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
cipher AES-256-CBC
auth SHA256
verb 3
EOF
chmod 0600 "$test_config"
run_vpn_switch openvpn import "$test_config" >/dev/null 2>&1
run_vpn_switch openvpn add work us-server.ovpn server >/dev/null 2>&1
# Run start command
local output=$(run_vpn_switch openvpn start work 2>&1 || true)
# Verify session was created
if printf '%s\n' "$output" | grep -qi "session"; then
pass "Session creation logged"
else
fail "Session creation not logged"
printf '%s\n' "Output: $output"
fi
# Verify openvpn command is in generated connect.sh
local session_dir=$(ls -1d "$TEST_DIR/.session"/* 2>/dev/null | head -1)
if [ -f "$session_dir/connect.sh" ] && grep -q "openvpn" "$session_dir/connect.sh"; then
pass "openvpn command in connect.sh"
else
fail "openvpn command not found in connect.sh"
printf '%s\n' "Output: $output"
fi
# Verify session directory was created
local session_count=$(ls -1d "$TEST_DIR/.session"/* 2>/dev/null | wc -l)
if [ "$session_count" -ge 1 ]; then
pass "Session directory created in .session/"
else
fail "Session directory not found"
ls -la "$TEST_DIR/.session/" || true
fi
# Verify session metadata files exist
local session_dir=$(ls -1d "$TEST_DIR/.session"/* 2>/dev/null | head -1)
if [ -f "$session_dir/protocol" ] && [ -f "$session_dir/pid" ]; then
pass "Session metadata files created"
else
fail "Session metadata files missing"
ls -la "$session_dir" || true
fi
# Verify connect.sh was generated
if [ -f "$session_dir/connect.sh" ]; then
pass "connect.sh script generated"
else
fail "connect.sh script not found"
ls -la "$session_dir" || true
fi
# Verify connect.sh is executable
if [ -x "$session_dir/connect.sh" ]; then
pass "connect.sh is executable"
else
fail "connect.sh is not executable"
ls -la "$session_dir/connect.sh" || true
fi
# Verify patched config exists (uses test interface name)
if [ -f "$session_dir/${test_interface}.ovpn" ]; then
pass "Patched config (${test_interface}.ovpn) created"
else
fail "Patched config not found"
ls -la "$session_dir" || true
fi
# Verify patched config contains daemon mode
if grep -q "daemon" "$session_dir/${test_interface}.ovpn"; then
pass "Patched config contains daemon mode"
else
fail "Patched config missing daemon mode"
grep -i "daemon" "$session_dir/${test_interface}.ovpn" || true
fi
# Phase-based approach: up/down scripts should be stripped (DNS handled by DNS phase)
if grep -q "^up " "$session_dir/${test_interface}.ovpn"; then
fail "Patched config should NOT contain up script (phase-based DNS)"
grep -i "up" "$session_dir/${test_interface}.ovpn" || true
else
pass "Patched config correctly strips up script (phase-based DNS)"
fi
if grep -q "^down " "$session_dir/${test_interface}.ovpn"; then
fail "Patched config should NOT contain down script (phase-based DNS)"
grep -i "down" "$session_dir/${test_interface}.ovpn" || true
else
pass "Patched config correctly strips down script (phase-based DNS)"
fi
# Cleanup
rm -f "$test_config"
}
# TEST: openvpn stop - stop all OpenVPN sessions
test_openvpn_stop_basic() {
test_header "openvpn stop - cleanup commands for session" "test_openvpn_stop_basic"
# Create exclusive database for this test
test_setup
# Set up mocks for network operations with unique test ID
local test_id=85
local expected_interface="tun_test${test_id}"
setup_session_test_mocks "$test_id"
# Create session through user interface (mocked network operations)
run_vpn_switch openvpn add work >/dev/null 2>&1
local test_config="$TEST_BASE_DIR/ovpn-test-stop.ovpn"
cat > "$test_config" <<'EOF'
client
dev tun
proto udp
remote vpn.example.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
cipher AES-256-CBC
auth SHA256
verb 3
EOF
chmod 0600 "$test_config"
run_vpn_switch openvpn import "$test_config" >/dev/null 2>&1
run_vpn_switch openvpn add work ovpn-test-stop.ovpn server >/dev/null 2>&1
run_vpn_switch openvpn start work >/dev/null 2>&1
# Verify session was created
local session_count=$(ls -1d "$TEST_DIR/.session"/* 2>/dev/null | wc -l)
if [ "$session_count" -ge 1 ]; then
pass "Session created via user interface"
else
fail "No session found after start"
return
fi
# Set interpreter to cat for command inspection (instead of execution)
override_for_command_inspection "openvpn_disconnect1"
# Call disconnect directly with the test interface
# This bypasses is_session_alive() and lets us inspect generated commands
local output=$(run_vpn_switch openvpn disconnect "$expected_interface" 2>&1 || true)
# Verify the generated commands include interface reference
if printf '%s\n' "$output" | grep -q "$expected_interface"; then
pass "Disconnect commands reference correct interface ($expected_interface)"
else
fail "Disconnect should reference interface $expected_interface"
printf '%s\n' "Output: $output"
return
fi
# Verify the generated commands include process termination
if printf '%s\n' "$output" | grep -q "kill"; then
pass "Disconnect generates process kill commands"
else
fail "Disconnect should generate kill commands"
printf '%s\n' "Output: $output"
fi
}
# TEST: openvpn disconnect - tear down interface
test_openvpn_disconnect_interface() {
test_header "openvpn disconnect - tear down OpenVPN interface" "test_openvpn_disconnect_interface"
test_setup
override_for_command_inspection "openvpn_disconnect1"
# Create a fake session directory
local session_dir="$TEST_DIR/.session/99999"
mkdir -p "$session_dir"
echo "tun0" > "$session_dir/interface"
echo "openvpn" > "$session_dir/protocol"
echo "12345" > "$session_dir/openvpn.pid"
echo "$TEST_DIR/openvpn/test.ovpn" > "$session_dir/original"
chmod 0600 "$session_dir"/*
# Run disconnect with interface name (not PID)
local output=$(run_vpn_switch openvpn disconnect tun0 2>&1 || true)
# Verify disconnect generates kill commands
if printf '%s\n' "$output" | grep -qi "kill"; then
pass "Disconnect generates process termination commands"
else
fail "Disconnect missing termination logic"
printf '%s\n' "Output: $output"
fi
}
# TEST: openvpn clean - generate cleanup commands
test_openvpn_clean() {
test_header "openvpn clean - generate cleanup commands for broken links" "test_openvpn_clean"
test_setup
override_for_command_inspection "openvpn_clean"
# Enable execution for openvpn add (to create symlinks)
set_function_interpreter "openvpn_add" "sh"
# Create a category with a broken symlink
run_vpn_switch openvpn add broken >/dev/null 2>&1
ln -s "$TEST_DIR/openvpn/nonexistent.ovpn" "$TEST_DIR/openvpn/broken/link1"
# Run clean command
local output=$(run_vpn_switch openvpn clean 2>&1)
# Should generate remove commands for broken links
if printf '%s\n' "$output" | grep -q "openvpn remove"; then
pass "Clean generates remove commands"
else
fail "Clean missing remove commands"
printf '%s\n' "Output: $output"
fi
# Verify it found the broken link
if printf '%s\n' "$output" | grep -qE "link1|nonexistent"; then
pass "Clean detected broken symlink"
else
fail "Clean didn't detect broken link"
printf '%s\n' "Output: $output"
fi
}
#-----------------------------------------------------------------------------
# Environment Management Tests
#-----------------------------------------------------------------------------
test_setenv_basic() {
test_header "setenv - basic variable setting" "test_setenv_basic"
test_setup
# Run setenv (will execute)
local output=$(run_vpn_switch setenv TEST_VAR "test_value" 2>&1)
# Check that .env/local directory was created
if [ -d "$TEST_DIR/.env/local" ]; then
pass ".env/local directory created"
else
fail ".env/local directory not created"
fi
# Check that file was created with correct content
if [ -f "$TEST_DIR/.env/local/TEST_VAR" ]; then
pass "Variable file created"
local content=$(head -n 1 "$TEST_DIR/.env/local/TEST_VAR")
if [ "$content" = "test_value" ]; then
pass "Variable has correct value"
else
fail "Variable has wrong value: $content"
fi
else
fail "Variable file not created"
fi
# Check permissions (use helper function for cross-platform)
local perms=$(get_file_perms_full "$TEST_DIR/.env/local/TEST_VAR")
if [ "$perms" = "100600" ] || [ "$perms" = "600" ] || [ "$perms" = "100640" ] || [ "$perms" = "640" ]; then
pass "File has correct permissions (0600 or 0640 for cross-user support)"
else
fail "File has wrong permissions: $perms"
fi
# Check for confirmation message
if printf '%s\n' "$output" | grep -q "Set TEST_VAR"; then
pass "Shows confirmation message"
else
fail "Missing confirmation message"
fi
}
test_setenv_invalid_name() {
test_header "setenv - invalid variable name" "test_setenv_invalid_name"
test_setup
# Run setenv with invalid name (contains slash) - expect exit code 1
local output=$(run_vpn_switch setenv "INVALID/VAR" "value" 2>&1 || true)
# Check for error message
if printf '%s\n' "$output" | grep -q "Error.*Invalid variable name"; then
pass "Rejects invalid variable name"
else
fail "Should reject invalid variable name"
fi
# Verify file was not created
if [ ! -f "$TEST_DIR/.env/local/INVALID/VAR" ]; then
pass "No file created for invalid name"
else
fail "File should not be created for invalid name"
fi
}
test_getenv_local_override() {
test_header "getenv - shows local override" "test_getenv_local_override"
test_setup
# Create local override file
mkdir -p "$TEST_DIR/.env/local"
echo "local_value" > "$TEST_DIR/.env/local/TEST_VAR"
# Run getenv (will execute)
local output=$(run_vpn_switch getenv TEST_VAR 2>&1)
# Check for local override message in stderr
if printf '%s\n' "$output" | grep -q "Source.*\.env/local.*override"; then
pass "Shows local override source"
else
fail "Missing source indicator"
fi
# Check that it outputs the value
if printf '%s\n' "$output" | grep -q "local_value"; then
pass "Outputs correct value from local"
else
fail "Missing value output"
fi
}
test_getenv_fallback_chain() {
test_header "getenv - fallback chain (local > default > template)" "test_getenv_fallback_chain"
test_setup
# Test 1: Variable not found anywhere
local output=$(run_vpn_switch getenv NONEXISTENT 2>&1 || true)
if printf '%s\n' "$output" | grep -q "not found"; then
pass "Shows error if variable not found"
else
fail "Missing error message"
fi
# Test 2: Variable in default only
mkdir -p "$TEST_DIR/.env/default"
echo "default_value" > "$TEST_DIR/.env/default/TEST_VAR_DEFAULT"
output=$(run_vpn_switch getenv TEST_VAR_DEFAULT 2>&1)
if printf '%s\n' "$output" | grep -q "Source.*\.env/default.*default"; then
pass "Falls back to .env/default"
else
fail "Missing .env/default fallback"
fi
# Test 3: Variable in template only
# VPN_SWITCH_TEMPLATE_DIR is set by bootstrap (points to template/)
local template_dir="${VPN_SWITCH_TEMPLATE_DIR:-$(dirname "$0")/template}"
echo "template_value" > "$template_dir/environment/TEST_VAR_TEMPLATE"
output=$(run_vpn_switch getenv TEST_VAR_TEMPLATE 2>&1)
if printf '%s\n' "$output" | grep -q "Source.*template/environment.*template"; then
pass "Falls back to template/environment"
else
fail "Missing template/environment fallback"
fi
rm -f "$template_dir/environment/TEST_VAR_TEMPLATE"
}
test_unsetenv_existing() {
test_header "unsetenv - remove existing variable" "test_unsetenv_existing"
test_setup
# Create local variable file
mkdir -p "$TEST_DIR/.env/local"
echo "value" > "$TEST_DIR/.env/local/TEST_VAR"
# Run unsetenv (will execute)
local output=$(run_vpn_switch unsetenv TEST_VAR 2>&1)
# Check that file was removed
if [ ! -f "$TEST_DIR/.env/local/TEST_VAR" ]; then
pass "Variable file was removed"
else
fail "Variable file still exists"
fi
# Check for confirmation message
if printf '%s\n' "$output" | grep -q "Unset TEST_VAR"; then
pass "Shows confirmation message"
else
fail "Missing confirmation message"
fi
}
test_unsetenv_not_set() {
test_header "unsetenv - variable not set in local" "test_unsetenv_not_set"
test_setup
# Run unsetenv on non-existent variable
local output=$(run_vpn_switch unsetenv NONEXISTENT_VAR 2>&1)
# Check for informative message
if printf '%s\n' "$output" | grep -q "not set"; then
pass "Shows informative message when not set"
else
fail "Missing informative message"
fi
}
test_dumpenv_prologue() {
test_header "dumpenv prologue - outputs safe minimal set" "test_dumpenv_prologue"
test_setup
# Set up test environment variables
# Prologue variables (base interpreters + settings)
run_vpn_switch setenv VPN_SWITCH_TERMINAL_INTERPRETER "cat" > /dev/null 2>&1
run_vpn_switch setenv VPN_SWITCH_DISPLAY_ANSI "1" > /dev/null 2>&1
# Epilogue-only variables (function-specific interpreters)
run_vpn_switch setenv VPN_SWITCH_INTERPRETER_wireguard_list "cat" > /dev/null 2>&1
run_vpn_switch setenv VPN_SWITCH_INTERPRETER_wireguard_import1 "cat" > /dev/null 2>&1
# Run dumpenv prologue
local output=$(run_vpn_switch dumpenv prologue 2>&1)
# Should include TERMINAL_INTERPRETER (base interpreter - in prologue)
if printf '%s\n' "$output" | grep -q "setenv VPN_SWITCH_TERMINAL_INTERPRETER"; then
pass "Prologue includes VPN_SWITCH_TERMINAL_INTERPRETER"
else
fail "Prologue missing VPN_SWITCH_TERMINAL_INTERPRETER"
fi
# Should include DISPLAY_ANSI (setting - in prologue)
if printf '%s\n' "$output" | grep -q "setenv VPN_SWITCH_DISPLAY_ANSI"; then
pass "Prologue includes VPN_SWITCH_DISPLAY_ANSI"
else
fail "Prologue missing VPN_SWITCH_DISPLAY_ANSI"
fi
# Should NOT include function-specific interpreters (epilogue-only)
if printf '%s\n' "$output" | grep -q "setenv VPN_SWITCH_INTERPRETER_wireguard_list"; then
fail "Prologue incorrectly includes function-specific interpreter"
else
pass "Prologue excludes VPN_SWITCH_INTERPRETER_wireguard_list (epilogue-only)"
fi
if printf '%s\n' "$output" | grep -q "setenv VPN_SWITCH_INTERPRETER_wireguard_import1"; then
fail "Prologue incorrectly includes function-specific interpreter"
else
pass "Prologue excludes VPN_SWITCH_INTERPRETER_wireguard_import1 (epilogue-only)"
fi
}
test_dumpenv_epilogue() {
test_header "dumpenv epilogue - outputs complete set" "test_dumpenv_epilogue"
test_setup
# Set up test environment variables (same as prologue test)
run_vpn_switch setenv VPN_SWITCH_TERMINAL_INTERPRETER "cat" > /dev/null 2>&1
run_vpn_switch setenv VPN_SWITCH_DISPLAY_ANSI "1" > /dev/null 2>&1
run_vpn_switch setenv VPN_SWITCH_INTERPRETER_wireguard_list "cat" > /dev/null 2>&1
run_vpn_switch setenv VPN_SWITCH_INTERPRETER_wireguard_import1 "cat" > /dev/null 2>&1
# Run dumpenv epilogue
local output=$(run_vpn_switch dumpenv epilogue 2>&1)
# Should include ALL variables (complete set)
if printf '%s\n' "$output" | grep -q "setenv VPN_SWITCH_TERMINAL_INTERPRETER"; then
pass "Epilogue includes VPN_SWITCH_TERMINAL_INTERPRETER"
else
fail "Epilogue missing VPN_SWITCH_TERMINAL_INTERPRETER"
fi
if printf '%s\n' "$output" | grep -q "setenv VPN_SWITCH_DISPLAY_ANSI"; then
pass "Epilogue includes VPN_SWITCH_DISPLAY_ANSI"
else
fail "Epilogue missing VPN_SWITCH_DISPLAY_ANSI"
fi
# Epilogue SHOULD include function-specific interpreters (complete set)
if printf '%s\n' "$output" | grep -q "setenv VPN_SWITCH_INTERPRETER_wireguard_list"; then
pass "Epilogue includes VPN_SWITCH_INTERPRETER_wireguard_list"
else
fail "Epilogue missing VPN_SWITCH_INTERPRETER_wireguard_list"
fi
if printf '%s\n' "$output" | grep -q "setenv VPN_SWITCH_INTERPRETER_wireguard_import1"; then
pass "Epilogue includes VPN_SWITCH_INTERPRETER_wireguard_import1"
else
fail "Epilogue missing VPN_SWITCH_INTERPRETER_wireguard_import1"
fi
}
test_dumpenv_profile_filtering() {
test_header "dumpenv - profile filtering prevents restore breakage" "test_dumpenv_profile_filtering"
test_setup
# Set a function-specific interpreter that would break restore if in prologue
run_vpn_switch setenv VPN_SWITCH_INTERPRETER_wireguard_import1 "cat" > /dev/null 2>&1
# Run both prologue and epilogue
local prologue_output=$(run_vpn_switch dumpenv prologue 2>&1)
local epilogue_output=$(run_vpn_switch dumpenv epilogue 2>&1)
# CRITICAL: Prologue must NOT have function-specific interpreters
# (otherwise imports would only display, not execute during restore)
if echo "$prologue_output" | grep -q "INTERPRETER_wireguard_import"; then
fail "CRITICAL: Prologue has function-specific interpreter (would break restore)"
else
pass "Prologue correctly excludes function-specific interpreter"
fi
# Epilogue SHOULD have function-specific interpreters (restored after restore completes)
if echo "$epilogue_output" | grep -q "INTERPRETER_wireguard_import"; then
pass "Epilogue correctly includes function-specific interpreter"
else
fail "Epilogue missing function-specific interpreter"
fi
}
#-----------------------------------------------------------------------------
# Environment Cache Tests (Phase 4.3)
#-----------------------------------------------------------------------------
# TEST 109: environment cache on - creates cache file
test_environment_cache_on() {
test_header "environment cache on - creates cache file with correct permissions" "test_environment_cache_on"
test_setup
# Enable cache
local output=$(run_vpn_switch environment cache on 2>&1)
# Check that cache file was created
local cache_file="$TEST_DIR/.env/local/VPN_SWITCH_CACHE_ENV_ARGS"
if [ -f "$cache_file" ]; then
pass "Cache file created"
else
fail "Cache file not created"
fi
# Check permissions (should be 0600) - use helper function for cross-platform
if [ -f "$cache_file" ]; then
local perms=$(get_file_perms_full "$cache_file")
if [ "$perms" = "100600" ] || [ "$perms" = "600" ]; then
pass "Cache file has correct permissions (0600)"
else
fail "Cache file has wrong permissions: $perms"
fi
fi
# Check that output shows success message
if printf '%s\n' "$output" | grep -q "Environment cache ENABLED"; then
pass "Shows cache enabled message"
else
fail "Missing cache enabled message"
fi
# Verify cache content is not empty
if [ -s "$cache_file" ]; then
pass "Cache file has content"
else
fail "Cache file is empty"
fi
}
# TEST 110: environment cache off - removes cache file
test_environment_cache_off() {
test_header "environment cache off - removes cache file" "test_environment_cache_off"
test_setup
# Enable cache first
run_vpn_switch environment cache on > /dev/null 2>&1
# Verify cache exists
local cache_file="$TEST_DIR/.env/local/VPN_SWITCH_CACHE_ENV_ARGS"
if [ ! -f "$cache_file" ]; then
fail "Setup failed: cache not created"
return 1
fi
# Disable cache
local output=$(run_vpn_switch environment cache off 2>&1)
# Check that cache file was removed
if [ ! -f "$cache_file" ]; then
pass "Cache file removed"
else
fail "Cache file still exists"
fi
# Check for confirmation message
if printf '%s\n' "$output" | grep -q "Environment cache DISABLED"; then
pass "Shows cache disabled message"
else
fail "Missing cache disabled message"
fi
}
# TEST 111: environment cache status - shows correct status
test_environment_cache_status() {
test_header "environment cache status - shows correct status" "test_environment_cache_status"
test_setup
# Check status when disabled
local output_disabled=$(run_vpn_switch environment cache status 2>&1)
if echo "$output_disabled" | grep -q "Environment cache: DISABLED"; then
pass "Shows DISABLED status when cache off"
else
fail "Incorrect status when cache off"
fi
# Enable cache
run_vpn_switch environment cache on > /dev/null 2>&1
# Check status when enabled
local output_enabled=$(run_vpn_switch environment cache status 2>&1)
if echo "$output_enabled" | grep -q "Environment cache: ENABLED"; then
pass "Shows ENABLED status when cache on"
else
fail "Incorrect status when cache on"
fi
# Check that variable count is shown
if echo "$output_enabled" | grep -q "vars)"; then
pass "Shows cached variable count"
else
fail "Missing cached variable count"
fi
}
# TEST 112: environment cache (no args) - defaults to status
test_environment_cache_no_args() {
test_header "environment cache (no args) - defaults to status" "test_environment_cache_no_args"
test_setup
# Run without arguments
local output=$(run_vpn_switch environment cache 2>&1)
# Should show status (not error)
if printf '%s\n' "$output" | grep -q "Environment cache:"; then
pass "Shows status with no arguments"
else
fail "Should show status with no arguments"
fi
}
# TEST 113: setenv rebuilds cache automatically
test_cache_invalidation_setenv() {
test_header "setenv - rebuilds cache automatically" "test_cache_invalidation_setenv"
test_setup
# Enable cache
run_vpn_switch environment cache on > /dev/null 2>&1
# Verify cache exists
local cache_file="$TEST_DIR/.env/local/VPN_SWITCH_CACHE_ENV_ARGS"
if [ ! -f "$cache_file" ]; then
fail "Setup failed: cache not created"
return 1
fi
# Run setenv (should rebuild cache automatically)
local output=$(run_vpn_switch setenv TEST_VAR "test_value" 2>&1)
# Check that cache was rebuilt (still exists with updated content)
if [ -f "$cache_file" ]; then
pass "Cache rebuilt automatically after setenv"
else
fail "Cache should be rebuilt (not deleted) after setenv"
fi
# Check that rebuilt cache contains the new variable
if [ -f "$cache_file" ] && grep -q "TEST_VAR" "$cache_file"; then
pass "Rebuilt cache contains new variable TEST_VAR"
else
fail "Rebuilt cache should contain new variable TEST_VAR"
fi
}
# TEST 114: unsetenv rebuilds cache automatically
test_cache_invalidation_unsetenv() {
test_header "unsetenv - rebuilds cache automatically" "test_cache_invalidation_unsetenv"
test_setup
# Set a variable first
run_vpn_switch setenv TEST_VAR "test_value" > /dev/null 2>&1
# Enable cache
run_vpn_switch environment cache on > /dev/null 2>&1
# Verify cache exists and contains TEST_VAR
local cache_file="$TEST_DIR/.env/local/VPN_SWITCH_CACHE_ENV_ARGS"
if [ ! -f "$cache_file" ]; then
fail "Setup failed: cache not created"
return 1
fi
# Verify cache contains TEST_VAR before unsetenv
if ! grep -q "TEST_VAR" "$cache_file"; then
fail "Setup failed: cache should contain TEST_VAR"
return 1
fi
# Run unsetenv (should rebuild cache automatically)
local output=$(run_vpn_switch unsetenv TEST_VAR 2>&1)
# Check that cache was rebuilt (still exists with updated content)
if [ -f "$cache_file" ]; then
pass "Cache rebuilt automatically after unsetenv"
else
fail "Cache should be rebuilt (not deleted) after unsetenv"
fi
# Check that rebuilt cache no longer contains TEST_VAR
if [ -f "$cache_file" ] && ! grep -q "TEST_VAR" "$cache_file"; then
pass "Rebuilt cache no longer contains TEST_VAR"
else
fail "Rebuilt cache should not contain TEST_VAR after unsetenv"
fi
}
# TEST 115: environment init invalidates cache
test_cache_invalidation_environment_init() {
test_header "environment init - invalidates cache automatically" "test_cache_invalidation_environment_init"
test_setup
# Enable cache
run_vpn_switch environment cache on > /dev/null 2>&1
# Verify cache exists
local cache_file="$TEST_DIR/.env/local/VPN_SWITCH_CACHE_ENV_ARGS"
if [ ! -f "$cache_file" ]; then
fail "Setup failed: cache not created"
return 1
fi
# Run environment init (should invalidate cache)
local output=$(run_vpn_switch environment init minimal 2>&1)
# Check that cache was removed
if [ ! -f "$cache_file" ]; then
pass "Cache file removed after environment init"
else
fail "Cache file should be removed after environment init"
fi
# Check for warning message
if printf '%s\n' "$output" | grep -q "Cache invalidated"; then
pass "Shows cache invalidation warning"
else
fail "Missing cache invalidation warning"
fi
}
# TEST 116: dumpenv places environment refresh after the epilogue
test_dumpenv_cache_preservation() {
test_header "dumpenv epilogue - ends with environment refresh (the pins hold for the lines after it)" "test_dumpenv_cache_preservation"
test_setup
run_vpn_switch environment cache on > /dev/null 2>&1
local output=$(run_vpn_switch dumpenv epilogue 2>&1)
if printf '%s\n' "$output" | grep -q 'VPN_SWITCH_CACHE_ENV_ARGS'; then
fail "Epilogue copied the cache"
else
pass "Epilogue never copies the cache"
fi
if [ "$(printf '%s\n' "$output" | grep -v '^$' | tail -n 1)" = '"$VPN_SWITCH_CONTEXT_SCRIPT" environment refresh' ]; then
pass "Epilogue ends with environment refresh"
else
fail "Epilogue does not end with environment refresh: $(printf '%s\n' "$output" | tail -n 2 | tr '\n' ' ')"
fi
}
# TEST 117: dumpenv places environment refresh without a cache too
test_dumpenv_cache_not_included_when_disabled() {
test_header "dumpenv epilogue - environment refresh is placed with the cache off as well" "test_dumpenv_cache_not_included_when_disabled"
test_setup
local output=$(run_vpn_switch dumpenv epilogue 2>&1)
if [ "$(printf '%s\n' "$output" | grep -v '^$' | tail -n 1)" = '"$VPN_SWITCH_CONTEXT_SCRIPT" environment refresh' ]; then
pass "Epilogue ends with environment refresh"
else
fail "Epilogue does not end with environment refresh: $(printf '%s\n' "$output" | tail -n 2 | tr '\n' ' ')"
fi
}
# TEST 118: cache does not include itself (recursion prevention)
test_cache_no_recursion() {
test_header "environment cache on - excludes cache file from scan" "test_cache_no_recursion"
test_setup
# Enable cache
run_vpn_switch environment cache on > /dev/null 2>&1
local cache_file="$TEST_DIR/.env/local/VPN_SWITCH_CACHE_ENV_ARGS"
if [ ! -f "$cache_file" ]; then
fail "Setup failed: cache not created"
return 1
fi
# Read cache content
local cache_content=$(cat "$cache_file")
# Cache should not reference itself (would cause recursion)
if echo "$cache_content" | grep -q "VPN_SWITCH_CACHE_ENV_ARGS="; then
fail "Cache includes itself (recursion risk)"
else
pass "Cache correctly excludes itself"
fi
}
#-----------------------------------------------------------------------------
# Inspect Command Tests (Phase 4.1 - Feature 3)
#-----------------------------------------------------------------------------
# TEST 109: inspect - batch command includes all sections
test_inspect_batch_complete() {
test_header "inspect (batch) - includes all inspection sections, keeps going past a failing one" "test_inspect_batch_complete"
test_setup
# an invalid config makes the validation section fail; a diagnostic reports that and goes on
: > "$TEST_DIR/openvpn/broken.ovpn"
local output=$(run_vpn_switch inspect 2>&1)
if printf '%s\n' "$output" | grep -q "^Summary$" && printf '%s\n' "$output" | grep -q "Complete diagnostic captured"; then
pass "Batch inspect reaches the summary with a failing section before it"
else
fail "Batch inspect stopped before the summary"
fi
# Verify all sections are included
if printf '%s\n' "$output" | grep -q "Environment Configuration"; then
pass "Batch inspect includes environment section"
else
fail "Batch inspect missing environment section"
fi
if printf '%s\n' "$output" | grep -q "System Configuration"; then
pass "Batch inspect includes system section"
else
fail "Batch inspect missing system section"
fi
if printf '%s\n' "$output" | grep -q "WireGuard.*Config"; then
pass "Batch inspect includes WireGuard section"
else
fail "Batch inspect missing WireGuard section"
fi
if printf '%s\n' "$output" | grep -q "OpenVPN.*Config"; then
pass "Batch inspect includes OpenVPN section"
else
fail "Batch inspect missing OpenVPN section"
fi
if printf '%s\n' "$output" | grep -qE "Session.*Status|Active.*Session"; then
pass "Batch inspect includes session section"
else
fail "Batch inspect missing session section"
fi
if printf '%s\n' "$output" | grep -qE "Phase.*Configuration|Phase.*Connect|Phase.*Disconnect"; then
pass "Batch inspect includes phases section"
else
fail "Batch inspect missing phases section"
fi
}
# TEST 110: environment inspect - shows key configuration
test_environment_inspect() {
test_header "environment inspect - displays environment variables" "test_environment_inspect"
test_setup
# Run environment inspect
local output=$(run_vpn_switch environment inspect)
# Verify header
if printf '%s\n' "$output" | grep -q "Environment Configuration"; then
pass "Environment inspect shows header"
else
fail "Environment inspect missing header"
fi
# Verify it shows VPN_SWITCH_BASE
if printf '%s\n' "$output" | grep -q "VPN_SWITCH_BASE"; then
pass "Environment inspect shows VPN_SWITCH_BASE"
else
fail "Environment inspect missing VPN_SWITCH_BASE"
fi
# Verify it shows database structure
if printf '%s\n' "$output" | grep -q "Database Structure"; then
pass "Environment inspect shows database structure"
else
fail "Environment inspect missing database structure"
fi
}
# TEST 111: system inspect - shows system information
test_system_inspect() {
test_header "system inspect - displays system configuration" "test_system_inspect"
test_setup
# Run system inspect
local output=$(run_vpn_switch system inspect)
# Verify header
if printf '%s\n' "$output" | grep -q "System Configuration"; then
pass "System inspect shows header"
else
fail "System inspect missing header"
fi
# Verify it shows system info
if printf '%s\n' "$output" | grep -q "System Info"; then
pass "System inspect shows system info section"
else
fail "System inspect missing system info"
fi
# Verify it shows network interfaces
if printf '%s\n' "$output" | grep -q "Network Interfaces"; then
pass "System inspect shows network interfaces section"
else
fail "System inspect missing network interfaces"
fi
}
# Issue #001 Tests: Variable Expansion Timing and Function Classification
# =========================================================================
# TEST 112: Batch combinator syntax validation (Issue #001)
test_batch_syntax_wireguard_clean() {
test_header "wireguard clean - validates batch syntax" "test_batch_syntax_wireguard_clean"
test_setup
# Override interpreter to cat (prevent execution, just show output)
override_for_command_inspection "wireguard_clean"
# Create broken symlink for clean to detect
mkdir -p "$TEST_DIR/wireguard"
ln -s /nonexistent "$TEST_DIR/wireguard/broken.conf"
# Get clean output (commands only, not executed)
local output=$(run_vpn_switch wireguard clean)
# Validate syntax using helper
if validate_batch_syntax "$output" "wireguard clean"; then
pass "wireguard clean outputs valid batch syntax"
else
fail "wireguard clean has invalid batch syntax"
fi
# Verify literal variable (not expanded)
if printf '%s\n' "$output" | grep -q '"\$VPN_SWITCH_CONTEXT_SCRIPT"'; then
pass "wireguard clean uses literal \$VPN_SWITCH_CONTEXT_SCRIPT"
else
fail "wireguard clean variable expansion error"
fi
}
# TEST 113: Combinators use literal context script (Issue #001)
test_combinators_literal_variable() {
test_header "combinators - use literal \$VPN_SWITCH_CONTEXT_SCRIPT" "test_combinators_literal_variable"
test_setup
# Override interpreter to cat for inspection
override_for_command_inspection "wireguard_start"
# Create test config
echo "[Interface]" > "$TEST_DIR/wireguard/test.conf"
# Test wireguard start combinator
local output=$(run_vpn_switch wireguard start test.conf 2>&1 || true)
# Must contain literal variable
if printf '%s\n' "$output" | grep -q '"\$VPN_SWITCH_CONTEXT_SCRIPT"'; then
pass "wireguard start uses literal variable"
else
fail "wireguard start variable not literal"
fi
# Must NOT contain expanded path
if ! printf '%s\n' "$output" | grep -qE '"/.*vpn-switch\.sh"'; then
pass "wireguard start variable not expanded"
else
fail "wireguard start variable was expanded"
fi
}
# TEST 114: Function naming matches behavior (Issue #001)
test_function_classification_correct() {
test_header "function classification - naming matches behavior" "test_function_classification_correct"
# Check wireguard add2 is combinator (double underscore)
if grep -q '^__wireguard_add2()' include/wireguard.sh; then
pass "wireguard add2 correctly classified as combinator (__)"
else
fail "wireguard add2 has wrong classification"
fi
# Check clean functions are batch combinators (triple underscore)
if grep -q '^___wireguard_clean0()' include/wireguard.sh; then
pass "wireguard clean0 correctly classified as batch (___)"
else
fail "wireguard clean0 has wrong classification"
fi
if grep -q '^___openvpn_clean0()' include/openvpn.sh; then
pass "openvpn clean0 correctly classified as batch (___)"
else
fail "openvpn clean0 has wrong classification"
fi
}
# TEST 115: No terminal functions output vpn-switch commands (Issue #001)
test_no_terminal_output_vpnswitch_commands() {
test_header "terminal functions - must not output vpn-switch commands" "test_no_terminal_output_vpnswitch_commands"
local violations=""
# Check main script
for func in $(grep '^_[^_][a-z0-9_]*[0-9]*()' vpn-switch.sh | cut -d'(' -f1); do
if awk "/^$func\(\)/,/^}/" vpn-switch.sh | grep -q 'echo.*"\$VPN_SWITCH_CONTEXT_SCRIPT"'; then
violations="$violations vpn-switch.sh:$func"
fi
done
# Check modules
for file in include/*.sh; do
for func in $(grep '^_[^_][a-z0-9_]*[0-9]*()' "$file" 2>/dev/null | cut -d'(' -f1); do
if awk "/^$func\(\)/,/^}/" "$file" | grep -q 'echo.*"\$VPN_SWITCH_CONTEXT_SCRIPT"'; then
violations="$violations $(basename $file):$func"
fi
done
done
if [ -z "$violations" ]; then
pass "No terminal functions output vpn-switch commands"
else
fail "Terminal functions violate rule: $violations"
fi
}
# TEST 116: Metadata consistency after renames (Issue #001)
test_metadata_function_lists() {
test_header "metadata - function lists accurate after renames" "test_metadata_function_lists"
# Check combinators list includes renamed wireguard_add2
if grep 'COMBINATOR_FUNCTIONS=' vpn-switch.sh | grep -q '__wireguard_add2'; then
pass "Metadata includes __wireguard_add2 in combinators"
else
fail "Metadata missing __wireguard_add2"
fi
# Check batch combinators list includes renamed clean functions
if grep 'BATCH_COMBINATOR_FUNCTIONS=' vpn-switch.sh | grep -q '___wireguard_clean0'; then
pass "Metadata includes ___wireguard_clean0 in batch combinators"
else
fail "Metadata missing ___wireguard_clean0"
fi
if grep 'BATCH_COMBINATOR_FUNCTIONS=' vpn-switch.sh | grep -q '___openvpn_clean0'; then
pass "Metadata includes ___openvpn_clean0 in batch combinators"
else
fail "Metadata missing ___openvpn_clean0"
fi
}
# TEST 117: Combinator syntax validation (Issue #001)
test_combinator_syntax_validation() {
test_header "combinators - validate single-command output syntax" "test_combinator_syntax_validation"
test_setup
# Override interpreter to cat for inspection
override_for_command_inspection "wireguard_start"
override_for_command_inspection "openvpn_start"
override_for_command_inspection "start"
# Create test configs
echo "[Interface]" > "$TEST_DIR/wireguard/test.conf"
echo "client" > "$TEST_DIR/openvpn/test.ovpn"
# Test wireguard start combinator
local wg_output=$(run_vpn_switch wireguard start test.conf 2>&1 || true)
# Combinator must output exactly ONE vpn-switch command
local wg_line_count=$(echo "$wg_output" | grep -c '"\$VPN_SWITCH_CONTEXT_SCRIPT"' || echo "0")
if [ "$wg_line_count" -eq 1 ]; then
pass "wireguard start outputs exactly one command"
else
fail "wireguard start should output 1 command, got $wg_line_count"
fi
# Must use literal variable
if echo "$wg_output" | grep -q '"\$VPN_SWITCH_CONTEXT_SCRIPT"'; then
pass "wireguard start uses literal variable"
else
fail "wireguard start missing literal variable"
fi
# Must NOT be expanded
if ! echo "$wg_output" | grep -qE '"/.*vpn-switch\.sh"'; then
pass "wireguard start variable not expanded"
else
fail "wireguard start variable was expanded"
fi
# Test openvpn start combinator
local ovpn_output=$(run_vpn_switch openvpn start test.ovpn 2>&1 || true)
local ovpn_line_count=$(echo "$ovpn_output" | grep -c '"\$VPN_SWITCH_CONTEXT_SCRIPT"' || echo "0")
if [ "$ovpn_line_count" -eq 1 ]; then
pass "openvpn start outputs exactly one command"
else
fail "openvpn start should output 1 command, got $ovpn_line_count"
fi
# Test protocol-agnostic start combinator
local start_output=$(run_vpn_switch start test.conf 2>&1 || true)
local start_line_count=$(echo "$start_output" | grep -c '"\$VPN_SWITCH_CONTEXT_SCRIPT"' || echo "0")
if [ "$start_line_count" -eq 1 ]; then
pass "start (protocol-agnostic) outputs exactly one command"
else
fail "start should output 1 command, got $start_line_count"
fi
}
# TEST 118: Error command with 1 line (Issue #002)
test_error_command_single_line() {
test_header "error command - single line message" "test_error_command_single_line"
test_setup
# Override to cat (display commands, not execute them)
set_function_interpreter "error" "cat"
# Run error with 1 line
local output=$(run_vpn_switch error "Something went wrong" 2>&1 || true)
# Verify it outputs printf command with error prefix
if printf '%s\n' "$output" | grep -q 'printf.*Error:.*Something went wrong'; then
pass "error command outputs printf with 'Error:' prefix"
else
fail "error command did not output expected printf command"
printf '%s\n' "Output: $output"
fi
# Verify it outputs exit 1
if printf '%s\n' "$output" | grep -q 'exit 1'; then
pass "error command outputs exit 1"
else
fail "error command did not output exit 1"
fi
# Verify output goes to stderr (>&2)
if printf '%s\n' "$output" | grep -q '>&2'; then
pass "error command redirects to stderr"
else
fail "error command did not redirect to stderr"
fi
}
# TEST 119: Error command with 2 lines (Issue #002)
test_error_command_two_lines() {
test_header "error command - two line message" "test_error_command_two_lines"
test_setup
# Override to cat (display commands, not execute them)
set_function_interpreter "error" "cat"
# Run error with 2 lines
local output=$(run_vpn_switch error "First line" "Second line" 2>&1 || true)
# Verify first line has Error: prefix
if printf '%s\n' "$output" | grep -q 'printf.*Error:.*First line'; then
pass "error command first line has 'Error:' prefix"
else
fail "error command first line incorrect"
printf '%s\n' "Output: $output"
fi
# Verify second line is indented (no Error: prefix)
if printf '%s\n' "$output" | grep -q 'printf.* %s.*Second line' && ! echo "$output" | tail -2 | grep -q 'Error:.*Second line'; then
pass "error command second line is indented without 'Error:' prefix"
else
fail "error command second line not properly formatted"
printf '%s\n' "Output: $output"
fi
# Verify exit 1
if printf '%s\n' "$output" | grep -q 'exit 1'; then
pass "error command outputs exit 1"
else
fail "error command did not output exit 1"
fi
}
# TEST 120: Error command with 3 lines (Issue #002)
test_error_command_three_lines() {
test_header "error command - three line message" "test_error_command_three_lines"
test_setup
# Override to cat (display commands, not execute them)
set_function_interpreter "error" "cat"
# Run error with 3 lines
local output=$(run_vpn_switch error "Line one" "Line two" "Line three" 2>&1 || true)
# Verify first line has Error: prefix
if printf '%s\n' "$output" | grep -q 'printf.*Error:.*Line one'; then
pass "error command first line has 'Error:' prefix"
else
fail "error command first line incorrect"
printf '%s\n' "Output: $output"
fi
# Verify second and third lines are indented (count printf with comment-prefixed indent pattern)
local indented_count=$(printf '%s\n' "$output" | grep 'printf "# %s' | wc -l | tr -d ' ')
if [ "$indented_count" -ge 2 ]; then
pass "error command has properly indented continuation lines"
else
fail "error command continuation lines not properly formatted (expected 2+ indented, got $indented_count)"
fi
# Verify all three lines are present
if printf '%s\n' "$output" | grep -q 'Line one' && printf '%s\n' "$output" | grep -q 'Line two' && printf '%s\n' "$output" | grep -q 'Line three'; then
pass "error command includes all three message lines"
else
fail "error command missing message lines"
printf '%s\n' "Output: $output"
fi
# Verify exit 1
if printf '%s\n' "$output" | grep -q 'exit 1'; then
pass "error command outputs exit 1"
else
fail "error command did not output exit 1"
fi
}
# TEST 121: Error command ANSI color support (Issue #002)
test_error_command_ansi_colors() {
test_header "error command - ANSI color support" "test_error_command_ansi_colors"
test_setup
# Enable ANSI colors via database (survives environment isolation)
run_vpn_switch setenv VPN_SWITCH_DISPLAY_ANSI 1 > /dev/null 2>&1
# Override to cat (display commands, not execute them)
set_function_interpreter "error" "cat"
# Run error with 1 line
local output=$(run_vpn_switch error "Test message" 2>&1 || true)
# Verify it contains ANSI escape codes when ANSI enabled
if printf '%s\n' "$output" | grep -q '\\033\['; then
pass "error command includes ANSI escape codes when enabled"
else
fail "error command did not include ANSI codes"
printf '%s\n' "Output: $output"
fi
# Verify reset code is present
if printf '%s\n' "$output" | grep -q '\\033\[0m'; then
pass "error command includes ANSI reset code"
else
fail "error command did not include ANSI reset code"
fi
}
# TEST 122: Regression test for wireguard add2 (Issue #001)
test_wireguard_add2_delegation() {
test_header "wireguard add2 - delegates correctly to add3" "test_wireguard_add2_delegation"
test_setup
# Override for command inspection (arity-specific to avoid breaking heterogeneous family)
override_for_command_inspection "wireguard_add2"
# Create test config
echo "[Interface]" > "$TEST_DIR/wireguard/test.conf"
mkdir -p "$TEST_DIR/wireguard/privacy"
# Run add2 (category + target, target used as alias)
local output=$(run_vpn_switch wireguard add privacy test.conf 2>&1 || true)
# Should delegate to add3 with target as both target and alias
if printf '%s\n' "$output" | grep -q '"\$VPN_SWITCH_CONTEXT_SCRIPT" wireguard add "privacy" "test.conf" "test.conf"'; then
pass "wireguard add2 delegates correctly with literal variable"
else
fail "wireguard add2 delegation incorrect"
fi
}
#-----------------------------------------------------------------------------
# Batch Infrastructure Tests (Phase 1 - Exit Code Encoding)
#-----------------------------------------------------------------------------
# TEST: get_batch_id - sequential allocation
test_batch_id_sequential() {
test_header "get_batch_id - sequential allocation" "test_batch_id_sequential"
test_setup
# Source vpn-switch.sh to access infrastructure functions
. "$TEST_SCRIPT"
# Get first batch ID
local batch_id_1=$(VPN_SWITCH_BASE="$TEST_DIR" LOG_FILE=/dev/null get_batch_id)
if [ "$batch_id_1" = "0" ]; then
pass "First batch ID is 0"
else
fail "First batch ID was $batch_id_1, expected 0"
fi
# Get second batch ID
local batch_id_2=$(VPN_SWITCH_BASE="$TEST_DIR" LOG_FILE=/dev/null get_batch_id)
if [ "$batch_id_2" = "1" ]; then
pass "Second batch ID is 1"
else
fail "Second batch ID was $batch_id_2, expected 1"
fi
# Get third batch ID
local batch_id_3=$(VPN_SWITCH_BASE="$TEST_DIR" LOG_FILE=/dev/null get_batch_id)
if [ "$batch_id_3" = "2" ]; then
pass "Third batch ID is 2"
else
fail "Third batch ID was $batch_id_3, expected 2"
fi
# Verify counter file exists and has correct value
local counter_value=$(cat "$TEST_DIR/.tmp/batch-counter")
if [ "$counter_value" = "3" ]; then
pass "Counter file has correct value (3)"
else
fail "Counter file has value $counter_value, expected 3"
fi
}
# TEST: store_batch_exits - basic storage
test_store_batch_exits_basic() {
test_header "store_batch_exits - stores exit codes to file" "test_store_batch_exits_basic"
test_setup
# Source vpn-switch.sh
. "$TEST_SCRIPT"
# Store batch exits
VPN_SWITCH_BASE="$TEST_DIR" LOG_FILE=/dev/null store_batch_exits 5 0 1 2 3
# Verify file created
if [ -f "$TEST_DIR/.tmp/batch-exits/5" ]; then
pass "Batch file created"
else
fail "Batch file not created"
fi
# Verify content
local content=$(cat "$TEST_DIR/.tmp/batch-exits/5")
if [ "$content" = "0 1 2 3" ]; then
pass "Batch file has correct content"
else
fail "Batch file content is '$content', expected '0 1 2 3'"
fi
}
# TEST: store_batch_exits - with batch references
test_store_batch_exits_references() {
test_header "store_batch_exits - stores exit codes with batch references" "test_store_batch_exits_references"
test_setup
# Source vpn-switch.sh
. "$TEST_SCRIPT"
# Store batch exits with references (format: "exit:batch_ref")
VPN_SWITCH_BASE="$TEST_DIR" LOG_FILE=/dev/null store_batch_exits 10 "0 5 129:1 0"
# Verify content includes batch references
local content=$(cat "$TEST_DIR/.tmp/batch-exits/10")
if [ "$content" = "0 5 129:1 0" ]; then
pass "Batch file stores references correctly"
else
fail "Batch file content is '$content', expected '0 5 129:1 0'"
fi
}
# TEST: free_batch_id - cleanup
test_free_batch_id() {
test_header "free_batch_id - deletes batch file" "test_free_batch_id"
test_setup
# Source vpn-switch.sh
. "$TEST_SCRIPT"
# Create a batch file
mkdir -p "$TEST_DIR/.tmp/batch-exits"
echo "0 1 0" > "$TEST_DIR/.tmp/batch-exits/7"
# Verify it exists
if [ -f "$TEST_DIR/.tmp/batch-exits/7" ]; then
pass "Batch file exists before cleanup"
else
fail "Batch file should exist before cleanup"
fi
# Free the batch ID
VPN_SWITCH_BASE="$TEST_DIR" LOG_FILE=/dev/null free_batch_id 7
# Verify file deleted
if [ ! -f "$TEST_DIR/.tmp/batch-exits/7" ]; then
pass "Batch file deleted after cleanup"
else
fail "Batch file still exists after cleanup"
fi
}
# TEST: Batch infrastructure - full lifecycle
test_batch_infrastructure_lifecycle() {
test_header "Batch infrastructure - full allocation/store/free lifecycle" "test_batch_infrastructure_lifecycle"
test_setup
# Source vpn-switch.sh
. "$TEST_SCRIPT"
# Allocate batch ID
local batch_id=$(VPN_SWITCH_BASE="$TEST_DIR" LOG_FILE=/dev/null get_batch_id)
pass "Allocated batch ID: $batch_id"
# Store results
VPN_SWITCH_BASE="$TEST_DIR" LOG_FILE=/dev/null store_batch_exits "$batch_id" "0 2 0 5"
if [ -f "$TEST_DIR/.tmp/batch-exits/$batch_id" ]; then
pass "Batch results stored"
else
fail "Batch results not stored"
fi
# Read and verify
local content=$(cat "$TEST_DIR/.tmp/batch-exits/$batch_id")
if [ "$content" = "0 2 0 5" ]; then
pass "Batch results readable"
else
fail "Batch results incorrect: '$content'"
fi
# Free batch ID
VPN_SWITCH_BASE="$TEST_DIR" LOG_FILE=/dev/null free_batch_id "$batch_id"
if [ ! -f "$TEST_DIR/.tmp/batch-exits/$batch_id" ]; then
pass "Batch cleaned up successfully"
else
fail "Batch file still exists after cleanup"
fi
}
# Sync command tests
#
# Validate the three new sync functions and the version status command.
# Tests use the public CLI surface only — no internal-state inspection.
test_sync_batch_outputs_three_subcommands() {
test_header "sync - batch combinator emits database init + phases/env/version sync" "test_sync_batch_outputs_three_subcommands"
test_setup
override_for_command_inspection "sync"
local output=$(run_vpn_switch sync)
if printf '%s\n' "$output" | grep -qE '"\$VPN_SWITCH_CONTEXT_SCRIPT" database init$'; then
pass "sync emits 'database init' first (an older database gains the layout)"
else
fail "sync missing 'database init' subcommand"
fi
if printf '%s\n' "$output" | grep -qE '"\$VPN_SWITCH_CONTEXT_SCRIPT" phases sync$'; then
pass "sync emits 'phases sync' subcommand"
else
fail "sync missing 'phases sync' subcommand"
fi
if printf '%s\n' "$output" | grep -qE '"\$VPN_SWITCH_CONTEXT_SCRIPT" env sync$'; then
pass "sync emits 'env sync' subcommand"
else
fail "sync missing 'env sync' subcommand"
fi
if printf '%s\n' "$output" | grep -qE '"\$VPN_SWITCH_CONTEXT_SCRIPT" version sync$'; then
pass "sync emits 'version sync' subcommand"
else
fail "sync missing 'version sync' subcommand"
fi
}
test_env_sync_per_file_chmod() {
test_header "env sync - 0600 per file without -R (regression guard)" "test_env_sync_per_file_chmod"
test_setup
override_for_command_inspection "env_sync"
local output=$(run_vpn_switch env sync)
# Regression guard: -R 0600 would strip the directory's traverse bit.
# Strip comment lines first so the explanatory comment in _env_sync0
# doesn't trigger a false positive.
local non_comment=$(printf '%s\n' "$output" | grep -v '^#')
if ! printf '%s\n' "$non_comment" | grep -qE 'chmod -R 0600|0600 -R'; then
pass "env sync does not use recursive chmod 0600"
else
fail "env sync uses recursive chmod 0600 (would lock out .env/default)"
fi
if printf "%s\n" "$output" | grep -qF "0600 \"$TEST_DIR/.env/default/\"*"; then
pass "env sync sets 0600 on the default files in one glob line"
else
fail "env sync missing the 0600 glob line for .env/default/*"
fi
}
test_environment_scan_ignores_stray_files() {
test_header "environment scan: a stray .new or backup file under .env/ is no variable" "test_environment_scan_ignores_stray_files"
test_setup
printf 'x\n' > "$TEST_DIR/.env/local/VPN_SWITCH_CACHE_ENV_ARGS.new"
printf 'y\n' > "$TEST_DIR/.env/local/VPN_SWITCH_DISPLAY_ANSI~"
run_vpn_switch environment cache off > /dev/null 2>&1
local out
if out=$(run_vpn_switch environment cache on 2>&1) && ! printf '%s\n' "$out" | grep -q "eval:" \
&& ! grep -q "VPN_SWITCH_CACHE_ENV_ARGS.new\|VPN_SWITCH_DISPLAY_ANSI~" "$TEST_DIR/.env/local/VPN_SWITCH_CACHE_ENV_ARGS"; then
pass "the cache rebuilt without the stray names"
else
fail "stray files reached the scan: $(printf '%s\n' "$out" | head -2 | tr '\n' ' ')"
fi
}
test_batch_pin_governs_next_line() {
test_header "batch: a pin set by one line governs the next line -- process runner and binary runner"
test_setup
local bin="$(dirname "$TEST_SCRIPT")/vpn-switch-binary.sh" ca="$TEST_BASE_DIR/pin-a-$TESTS_RUN.conf" cb="$TEST_BASE_DIR/pin-b-$TESTS_RUN.conf" out
printf '[Interface]\nPrivateKey = x\nAddress = 10.0.0.2/32\n[Peer]\nPublicKey = y\nEndpoint = 1.2.3.4:51820\nAllowedIPs = 0.0.0.0/0\n' > "$ca"
cp "$ca" "$cb"
cat > "$TEST_BASE_DIR/unit-pin-$TESTS_RUN.sh" <<EOF
"\$VPN_SWITCH_CONTEXT_SCRIPT" wireguard import $ca
"\$VPN_SWITCH_CONTEXT_SCRIPT" setenv VPN_SWITCH_INTERPRETER_wireguard_import1 cat
"\$VPN_SWITCH_CONTEXT_SCRIPT" environment refresh
"\$VPN_SWITCH_CONTEXT_SCRIPT" wireguard import $cb
EOF
out=$(run_vpn_switch batch "$TEST_BASE_DIR/unit-pin-$TESTS_RUN.sh" 2>&1)
if [ -f "$TEST_DIR/wireguard/${ca##*/}" ] && [ ! -f "$TEST_DIR/wireguard/${cb##*/}" ] \
&& printf '%s\n' "$out" | grep -q "^# Import WireGuard config: ${cb##*/}"; then
pass "process runner: the import before the pin ran, the one after it was displayed"
else
fail "process runner: $(ls "$TEST_DIR/wireguard" 2>&1 | tr '\n' ' ')"
fi
rm -f "$TEST_DIR/wireguard/${ca##*/}"
VPN_SWITCH_BASE="$TEST_DIR" "$TEST_SCRIPT" unsetenv VPN_SWITCH_INTERPRETER_wireguard_import1 > /dev/null 2>&1
out=$(VPN_SWITCH_DISPLAY_ANSI=0 sh "$bin" "$TEST_DIR" batch "$TEST_BASE_DIR/unit-pin-$TESTS_RUN.sh" 2>&1)
if [ -f "$TEST_DIR/wireguard/${ca##*/}" ] && [ ! -f "$TEST_DIR/wireguard/${cb##*/}" ] \
&& printf '%s\n' "$out" | grep -q "^# Import WireGuard config: ${cb##*/}"; then
pass "binary runner: the import before the pin ran, the one after it was displayed"
else
fail "binary runner: $(ls "$TEST_DIR/wireguard" 2>&1 | tr '\n' ' ')"
fi
}
test_binary_runs_in_one_process() {
test_header "vpn-switch-binary.sh: the engine's exit rules in one process -- a command, fail-fast, keep-going, the three names"
test_setup
local bin="$(dirname "$TEST_SCRIPT")/vpn-switch-binary.sh" walk="$(dirname "$TEST_SCRIPT")/vpn-switch-walkthrough.sh" out rc n
cat > "$TEST_BASE_DIR/unit-bin-$TESTS_RUN.sh" <<'EOF'
"$VPN_SWITCH_CONTEXT_SCRIPT" version
"$VPN_SWITCH_CONTEXT_SCRIPT" error boom
"$VPN_SWITCH_CONTEXT_SCRIPT" version
EOF
rc=0; out=$(sh "$bin" "$TEST_DIR" version 2>&1) || rc=$?
if [ "$rc" -eq 0 ] && printf '%s\n' "$out" | grep -q '^db:'; then
pass "a command runs and exits 0"
else
fail "version via the binary: rc=$rc $(printf '%s\n' "$out" | head -3 | tr '\n' '|')"
fi
rc=0; sh "$bin" "$TEST_DIR" error boom > /dev/null 2>&1 || rc=$?
if [ "$rc" -eq 1 ]; then pass "a failing command exits 1"; else fail "error via the binary: rc=$rc"; fi
rc=0; out=$(sh "$bin" "$TEST_DIR" batch "$TEST_BASE_DIR/unit-bin-$TESTS_RUN.sh" 2>&1) || rc=$?
n=$(printf '%s\n' "$out" | grep -c '^db:' || true)
if [ "$rc" -ne 0 ] && [ "$n" -eq 1 ]; then
pass "fail-fast: the line after the failure did not run, exit $rc"
else
fail "fail-fast: rc=$rc versions=$n $(printf '%s\n' "$out" | tail -3 | tr '\n' '|')"
fi
rc=0; out=$(VPN_SWITCH_BATCH_KEEP_GOING=1 sh "$bin" "$TEST_DIR" batch "$TEST_BASE_DIR/unit-bin-$TESTS_RUN.sh" 2>&1) || rc=$?
n=$(printf '%s\n' "$out" | grep -c '^db:' || true)
if [ "$rc" -ne 0 ] && [ "$n" -eq 2 ]; then
pass "keep-going: every line ran, exit $rc still reports the failure"
else
fail "keep-going: rc=$rc versions=$n"
fi
rc=0; out=$(sh "$walk" "$TEST_DIR" error boom 2>&1) || rc=$?
if printf '%s\n' "$out" | grep -q '^# -- _error1 error boom' && printf '%s\n' "$out" | grep -q '^exit 1'; then
pass "the walkthrough shows the act under its anchor instead of running it"
else
fail "walkthrough: rc=$rc $(printf '%s\n' "$out" | head -4 | tr '\n' '|')"
fi
}
test_version_sync_outputs_copy_and_perms() {
test_header "version sync - copies VERSION and chmods 0600" "test_version_sync_outputs_copy_and_perms"
test_setup
override_for_command_inspection "version_sync"
local output=$(run_vpn_switch version sync)
if printf '%s\n' "$output" | grep -qE 'template/VERSION.*\.version'; then
pass "version sync copies template/VERSION → .version"
else
fail "version sync missing copy of template/VERSION to .version"
fi
if printf '%s\n' "$output" | grep -qE '0600.*\.version|\.version.*0600'; then
pass "version sync chmods .version to 0600"
else
fail "version sync missing 0600 chmod on .version"
fi
}
test_version_command_stamped() {
test_header "version - reports db and source SHAs after bootstrap" "test_version_command_stamped"
test_setup
# Bootstrap auto-stamps .version, so freshly bootstrapped DB should report a real SHA
local output=$(run_vpn_switch version)
if printf '%s\n' "$output" | grep -qE '^db: [a-f0-9]{7,}$'; then
pass "version reports db SHA in 'db: <sha>' format"
else
fail "version missing 'db: <sha>' line (output: $output)"
fi
if printf '%s\n' "$output" | grep -qE '^source: [a-f0-9]{7,}$'; then
pass "version reports source SHA in 'source: <sha>' format"
else
fail "version missing 'source: <sha>' line"
fi
local db_sha=$(printf '%s\n' "$output" | grep '^db:' | awk '{print $2}')
local src_sha=$(printf '%s\n' "$output" | grep '^source:' | awk '{print $2}')
if [ -n "$db_sha" ] && [ "$db_sha" = "$src_sha" ]; then
pass "version: db SHA matches source SHA after fresh bootstrap"
else
fail "version: db SHA != source SHA (db=$db_sha source=$src_sha)"
fi
}
test_version_command_unstamped() {
test_header "version - reports 'unstamped' when .version is missing" "test_version_command_unstamped"
test_setup
# Remove the .version that bootstrap auto-stamped
rm -f "$TEST_DIR/.version"
local output=$(run_vpn_switch version)
if printf '%s\n' "$output" | grep -qE '^db: unstamped$'; then
pass "version reports 'db: unstamped' when .version absent"
else
fail "version did not report 'db: unstamped' (output: $output)"
fi
}
#-----------------------------------------------------------------------------
# Main Test Runner
#-----------------------------------------------------------------------------
# parallel_main - run every test function of the active filter as its own
# suite process (sequential inside), MAXPROCS at a time via xargs -P; then
# replay the outputs in list order and print an aggregated summary.
parallel_main() {
local outdir rc=0 t
outdir=$(mktemp -d "${TMPDIR:-/tmp}/vsw-par.XXXXXX") || exit 1
echo "vpn-switch Unit Test Suite (parallel, -P $MAXPROCS)"
echo ""
local tests
if [ "$TEST_FILTER" = "$ALL_TESTS" ]; then
# default run: the REAL test list are the should_run_test lines in main
# (ALL_TESTS is the grep-derived filter BASE and also catches the
# framework functions test_header/test_summary/test_setup)
tests=$(grep -E '^[[:space:]]*should_run_test ' "$0" | awk '{print $2}')
else
tests="$TEST_FILTER"
fi
printf '%s\n' $tests | xargs -n1 -P "$MAXPROCS" -I{} \
sh -c 'sh "$0" "$1" {} > "$2/{}.out" 2>&1; echo $? > "$2/{}.rc"' \
"$0" "$TEST_PROFILE" "$outdir"
for t in $tests; do
cat "$outdir/$t.out" 2>/dev/null
[ "$(cat "$outdir/$t.rc" 2>/dev/null)" = "0" ] || rc=1
done
echo ""
echo "========================================"
echo "Aggregated Summary (parallel run)"
echo "========================================"
awk '
/^Test Functions:/ { tf += $3 }
/^Total Assertions:/ { ta += $3 }
/Passed Assertions:/ { pa += $3 }
/Failed Assertions:/ { fa += $3 }
/Stories:$/ { sect = "s" }
/Assertions:$/ { sect = "a" }
/^ Passed:/ { if (sect == "s") sp += $2; else ap += $2 }
/^ Failed:/ { if (sect == "s") sf += $2; else fa += $2 }
END {
if (tf) printf "Test Functions: %d\n", tf
if (sp || sf) printf "Stories: %d passed, %d failed\n", sp, sf
if (ta) printf "Total Assertions: %d\n", ta
printf "Passed Assertions: %d\n", pa + ap
printf "Failed Assertions: %d\n", fa
}' "$outdir"/*.out
if [ "$rc" -eq 0 ]; then
echo ""
echo "ALL TESTS PASSED"
rm -rf "$outdir"
else
echo ""
echo "SOME TESTS FAILED"
echo "Per-test outputs preserved in: $outdir"
fi
return $rc
}
main() {
if [ "$MAXPROCS" -gt 1 ]; then
parallel_main
return $?
fi
printf '%b========================================%b\n' "$COLOR_BLUE" "$COLOR_RESET"
printf '%bvpn-switch Unit Test Suite (Formalized)%b\n' "$COLOR_BLUE" "$COLOR_RESET"
printf '%b========================================%b\n' "$COLOR_BLUE" "$COLOR_RESET"
echo ""
# Check prerequisites
if [ ! -f "$TEST_SCRIPT" ]; then
printf '%bERROR:%b %s not found\n' "$COLOR_RED" "$COLOR_RESET" "$TEST_SCRIPT"
echo "Run this script from the repository root"
exit 1
fi
echo ""
printf '%bRunning Tests%b\n' "$COLOR_BLUE" "$COLOR_RESET"
echo "========================================"
# Print filter info if active
if [ -n "$TEST_FILTER" ]; then
printf '%bFilter active - running only:%b%s\n' "$COLOR_YELLOW" "$COLOR_RESET" "$TEST_FILTER"
echo ""
fi
# Run tests (each test calls test_setup for exclusive database)
# No cleanup - artifacts preserved for debugging
# Batch infrastructure tests (Phase 1 - Exit Code Encoding)
should_run_test test_export_import_pair
should_run_test test_restore_pin_in_prologue
should_run_test test_batch_id_sequential
should_run_test test_store_batch_exits_basic
should_run_test test_store_batch_exits_references
should_run_test test_free_batch_id
should_run_test test_batch_infrastructure_lifecycle
# Terminal commands (simple display functions)
should_run_test test_printenv_command
should_run_test test_complete_command
should_run_test test_status_command
should_run_test test_error_command_single_line
should_run_test test_error_command_two_lines
should_run_test test_error_command_three_lines
should_run_test test_error_command_ansi_colors
should_run_test test_wireguard_list
should_run_test test_wireguard_validate
should_run_test test_wireguard_info
# Combinator commands (file operations)
should_run_test test_wireguard_import_valid
should_run_test test_wireguard_add_category
should_run_test test_wireguard_link_create
# Layer 2: VPN connection operations
should_run_test test_wireguard_start_basic
should_run_test test_wireguard_connect_resume
should_run_test test_wireguard_stop_basic
# Layer 3: Session management
should_run_test test_session_list
should_run_test test_session_refresh
should_run_test test_session_refresh_clean
should_run_test test_session_show
should_run_test test_session_show_by_name
should_run_test test_session_remove
# Layer 3: Session save/start (resumption)
should_run_test test_session_save_default
should_run_test test_session_save_named
should_run_test test_session_start_default
should_run_test test_session_start_named
should_run_test test_session_start_bypid
# Layer 3a: Session stop (active session management)
should_run_test test_session_stop_named
should_run_test test_session_stop_bypid
should_run_test test_session_stop_inactive
should_run_test test_session_stop_batch_all
# Config management: remove operations
should_run_test test_wireguard_remove_config_in_use
should_run_test test_wireguard_remove_config_with_refs
should_run_test test_wireguard_remove_symlink
should_run_test test_wireguard_remove_category_link
# Config management: clean operations
should_run_test test_wireguard_clean_no_broken
should_run_test test_wireguard_clean_broken_protocol_link
should_run_test test_wireguard_clean_broken_category_link
should_run_test test_wireguard_clean_category_no_broken
should_run_test test_wireguard_clean_category_with_broken
# Config management: validate operations
should_run_test test_wireguard_validate_broken_symlink
should_run_test test_wireguard_validate_broken_category_symlink
should_run_test test_wireguard_validate_naming_conflict
should_run_test test_wireguard_validate_invalid_config
# Stop/disconnect operations
should_run_test test_stop_no_sessions
should_run_test test_stop_specific_interface
should_run_test test_wireguard_disconnect_interface
should_run_test test_stop_nonexistent_interface
should_run_test test_disconnect_without_session
should_run_test test_stop_twice_idempotent
# Config management: import/add/link edge cases
should_run_test test_wireguard_import_nonexistent
should_run_test test_wireguard_import_duplicate
should_run_test test_wireguard_add_category_conflict
should_run_test test_wireguard_add_link_arity2
should_run_test test_wireguard_add_link_arity3
should_run_test test_wireguard_add_link_conflict
should_run_test test_wireguard_link_conflict
# Error handling
should_run_test test_error_invalid_category
should_run_test test_error_broken_symlink
should_run_test test_error_invalid_command
# Edge cases
should_run_test test_edge_duplicate_instance_prevention
should_run_test test_edge_stale_session_cleanup
# Config management: patch operations
should_run_test test_wireguard_patch_valid
should_run_test test_wireguard_patch_nonexistent
should_run_test test_wireguard_patch_absolute
should_run_test test_wireguard_patch_keepalive
# Config management: info operations
should_run_test test_wireguard_info_category
# Session management: save/start operations
should_run_test test_session_save_default
should_run_test test_session_save_named
should_run_test test_session_start_default
should_run_test test_session_start_named
should_run_test test_session_start_by_pid
# OpenVPN config management
should_run_test test_openvpn_import_valid
should_run_test test_openvpn_list
should_run_test test_openvpn_add_group
should_run_test test_openvpn_add_to_group_arity2
should_run_test test_openvpn_add_to_group_arity3
should_run_test test_openvpn_link
should_run_test test_openvpn_remove
should_run_test test_openvpn_validate
should_run_test test_openvpn_info
should_run_test test_openvpn_dump
# OpenVPN lifecycle tests (Phase 3.3.1)
should_run_test test_openvpn_patch_valid
should_run_test test_openvpn_patch_nonexistent
should_run_test test_openvpn_patch_absolute
should_run_test test_openvpn_patch_credentials_bare
should_run_test test_openvpn_patch_credentials_existing
should_run_test test_openvpn_patch_credentials_missing
should_run_test test_openvpn_start_basic
should_run_test test_openvpn_stop_basic
should_run_test test_openvpn_disconnect_interface
should_run_test test_openvpn_clean
# Protocol-agnostic wrappers (delegation tests)
should_run_test test_protocol_agnostic_import_wireguard
should_run_test test_protocol_agnostic_list
should_run_test test_protocol_agnostic_link
should_run_test test_protocol_agnostic_remove
should_run_test test_protocol_agnostic_validate
# System validation tests (Phase 4.1 - Feature 3)
should_run_test test_network_validate
should_run_test test_permissions_validate
should_run_test test_environment_validate
should_run_test test_batch_validate_includes_system
# Protocol-agnostic start tests (Phase 3.2.1)
should_run_test test_start_wireguard_only
should_run_test test_start_openvpn_only
should_run_test test_start_not_found
should_run_test test_start_no_default
should_run_test test_start_with_default_wireguard
should_run_test test_start_with_default_openvpn
should_run_test test_start_default_invalid_target
# Phase 3.4.1: Protocol discovery tests
should_run_test test_init_extension_links
should_run_test test_start_extension_link_fast_path
should_run_test test_start_multiple_matches
# Phase 4.4: Init function tests
should_run_test test_database_init
should_run_test test_environment_init
should_run_test test_environment_init_platform_template
should_run_test test_phases_copy
should_run_test test_phases_lift
should_run_test test_phases_default_profile
should_run_test test_phases_sync_single
should_run_test test_phases_sync_all
should_run_test test_init_batch_combinator
should_run_test test_init_combinator
# Environment management commands
should_run_test test_setenv_basic
should_run_test test_setenv_invalid_name
should_run_test test_getenv_local_override
should_run_test test_getenv_fallback_chain
should_run_test test_unsetenv_existing
should_run_test test_unsetenv_not_set
should_run_test test_dumpenv_prologue
should_run_test test_dumpenv_epilogue
should_run_test test_dumpenv_profile_filtering
# Environment cache tests (Phase 4.3)
should_run_test test_environment_cache_on
should_run_test test_environment_cache_off
should_run_test test_environment_cache_status
should_run_test test_environment_cache_no_args
should_run_test test_cache_invalidation_setenv
should_run_test test_cache_invalidation_unsetenv
should_run_test test_cache_invalidation_environment_init
should_run_test test_dumpenv_cache_preservation
should_run_test test_dumpenv_cache_not_included_when_disabled
should_run_test test_cache_no_recursion
# Interpreter resolution tests (lower half of lookup_interpreter)
should_run_test test_interpreter_single_underscore
should_run_test test_interpreter_double_underscore
should_run_test test_interpreter_triple_underscore
should_run_test test_interpreter_resolution_priority
# Inspect command tests (Phase 4.1 - Feature 3)
should_run_test test_inspect_batch_complete
should_run_test test_environment_inspect
should_run_test test_system_inspect
# Issue #001: Variable Expansion Timing and Function Classification
should_run_test test_batch_syntax_wireguard_clean
should_run_test test_combinators_literal_variable
should_run_test test_combinator_syntax_validation
should_run_test test_function_classification_correct
should_run_test test_no_terminal_output_vpnswitch_commands
should_run_test test_metadata_function_lists
should_run_test test_wireguard_add2_delegation
# Sync command tests
should_run_test test_sync_batch_outputs_three_subcommands
should_run_test test_env_sync_per_file_chmod
should_run_test test_version_sync_outputs_copy_and_perms
should_run_test test_version_command_stamped
should_run_test test_version_command_unstamped
should_run_test test_environment_scan_ignores_stray_files
should_run_test test_batch_pin_governs_next_line
should_run_test test_binary_runs_in_one_process
# Print summary
test_summary
}
# Run tests
main "$@"