#!/usr/bin/env bash set -Eeuo pipefail umask 077 TESTS_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" readonly TESTS_DIR # shellcheck source=helpers.sh source "${TESTS_DIR}/helpers.sh" # shellcheck source=../wg-manager.sh source "${PROJECT_DIR}/wg-manager.sh" case $# in 0) ;; 1) case $1 in config|parser|status|list|list-redirected|list-deterministic) ;; *) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;; esac ;; *) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;; esac test_group=${1-} setup_test_environment write_valid_config() { local output_file=$1 override_key=${2-} override_value=${3-} local wg_interface=wg0 wg_conf="${TEST_ROOT}/wg0.conf" client_dir="${TEST_ROOT}/clients" local server_endpoint=vpn.example.invalid server_port=51820 server_vpn_ip=192.0.2.1 local client_dns='192.0.2.53,2001:db8::53' vpn_subnet_prefix=192.0.2 start_host=2 end_host=254 local use_ipv6=0 ipv6_prefix=2001:db8:ffff:: default_client_mode=full-tunnel local recent_handshake_seconds=180 backup_retention=10 case $override_key in '') ;; WG_INTERFACE) wg_interface=$override_value ;; WG_CONF) wg_conf=$override_value ;; CLIENT_DIR) client_dir=$override_value ;; SERVER_ENDPOINT) server_endpoint=$override_value ;; SERVER_PORT) server_port=$override_value ;; SERVER_VPN_IP) server_vpn_ip=$override_value ;; CLIENT_DNS) client_dns=$override_value ;; VPN_SUBNET_PREFIX) vpn_subnet_prefix=$override_value ;; START_HOST) start_host=$override_value ;; END_HOST) end_host=$override_value ;; USE_IPV6) use_ipv6=$override_value ;; IPV6_PREFIX) ipv6_prefix=$override_value ;; DEFAULT_CLIENT_MODE) default_client_mode=$override_value ;; RECENT_HANDSHAKE_SECONDS) recent_handshake_seconds=$override_value ;; BACKUP_RETENTION) backup_retention=$override_value ;; RANGE_REVERSED) start_host=254; end_host=2 ;; *) printf 'unknown test config key: %s\n' "$override_key" >&2; return 64 ;; esac cat >"$output_file" <"$temp_file" mv -- "$temp_file" "$input_file" } replace_config_line() { local input_file=$1 prefix=$2 replacement=$3 line temp_file temp_file="${input_file}.tmp" while IFS= read -r line || [[ -n $line ]]; do if [[ $line == "${prefix}"* ]]; then printf '%s\n' "$replacement"; else printf '%s\n' "$line"; fi done <"$input_file" >"$temp_file" mv -- "$temp_file" "$input_file" } run_config_tests() { local valid_config="${TEST_ROOT}/valid.conf" status output marker write_valid_config "$valid_config" export WG_MANAGER_CONFIG="$valid_config" export WG_MANAGER_BACKUP_DIR="${TEST_ROOT}/backups" export WG_MANAGER_LOG_DIR="${TEST_ROOT}/logs" export WG_MANAGER_METADATA_DIR="${TEST_ROOT}/clients" export WG_MANAGER_ARCHIVE_DIR="${TEST_ROOT}/archive" export WG_MANAGER_LOCK_FILE="${TEST_ROOT}/manager.lock" begin_test 'valid restricted configuration loads with isolated paths' load_config assert_equal wg0 "$WG_INTERFACE" 'имя интерфейса' assert_equal "$TEST_ROOT/wg0.conf" "$WG_CONF" 'путь WG_CONF' assert_equal "$TEST_ROOT/clients" "$CLIENT_DIR" 'путь клиентов' assert_equal "$TEST_ROOT/backups" "$BACKUP_DIR" 'путь резервных копий' assert_equal "$TEST_ROOT/logs" "$LOG_DIR" 'путь журнала' assert_equal "$TEST_ROOT/clients" "$METADATA_DIR" 'путь метаданных' assert_equal "$TEST_ROOT/archive" "$ARCHIVE_DIR" 'путь архива' assert_equal "$TEST_ROOT/manager.lock" "$LOCK_FILE" 'путь lock-файла' begin_test 'required keys and parser quoting branches are covered' write_valid_config "${TEST_ROOT}/missing.conf" remove_config_key "${TEST_ROOT}/missing.conf" WG_INTERFACE WG_MANAGER_CONFIG="${TEST_ROOT}/missing.conf" set +e output=$(load_config 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'отсутствующий параметр' assert_contains 'Отсутствует параметр' "$output" 'диагностика отсутствующего параметра' write_valid_config "${TEST_ROOT}/quotes.conf" SERVER_ENDPOINT 'vpn.example.invalid"' WG_MANAGER_CONFIG="${TEST_ROOT}/quotes.conf" set +e output=$(load_config 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'встроенные кавычки' assert_contains 'Некорректные кавычки' "$output" 'диагностика кавычек' write_valid_config "${TEST_ROOT}/ipv6-on.conf" USE_IPV6 1 WG_MANAGER_CONFIG="${TEST_ROOT}/ipv6-on.conf" load_config assert_equal 1 "$USE_IPV6" 'включённый IPv6' write_valid_config "${TEST_ROOT}/whitespace.conf" replace_config_line "${TEST_ROOT}/whitespace.conf" 'SERVER_PORT=' ' SERVER_PORT = 51820 ' printf '\n# Full-line comment\n' >>"${TEST_ROOT}/whitespace.conf" WG_MANAGER_CONFIG="${TEST_ROOT}/whitespace.conf" load_config assert_equal 51820 "$SERVER_PORT" 'пробелы и комментарии' write_valid_config "${TEST_ROOT}/unquoted.conf" replace_config_line "${TEST_ROOT}/unquoted.conf" 'SERVER_PORT=' 'SERVER_PORT=51820' WG_MANAGER_CONFIG="${TEST_ROOT}/unquoted.conf" load_config assert_equal 51820 "$SERVER_PORT" 'неквотированное значение' begin_test 'runtime path defaults derive from SCRIPT_DIR' unset WG_MANAGER_CONFIG WG_MANAGER_BACKUP_DIR WG_MANAGER_LOG_DIR WG_MANAGER_METADATA_DIR WG_MANAGER_ARCHIVE_DIR WG_MANAGER_LOCK_FILE set_runtime_paths assert_equal "${SCRIPT_DIR}/wg-manager.conf" "$CONFIG_PATH" 'конфигурация по умолчанию' assert_equal "${SCRIPT_DIR}/backups" "$BACKUP_DIR" 'резервные копии по умолчанию' assert_equal "${SCRIPT_DIR}/logs" "$LOG_DIR" 'журнал по умолчанию' assert_equal "${SCRIPT_DIR}/state/clients" "$METADATA_DIR" 'метаданные по умолчанию' assert_equal "${SCRIPT_DIR}/state/archive" "$ARCHIVE_DIR" 'архив по умолчанию' assert_equal "${SCRIPT_DIR}/state/wg-manager.lock" "$LOCK_FILE" 'lock по умолчанию' export WG_MANAGER_CONFIG="$valid_config" export WG_MANAGER_BACKUP_DIR="${TEST_ROOT}/backups" export WG_MANAGER_LOG_DIR="${TEST_ROOT}/logs" export WG_MANAGER_METADATA_DIR="${TEST_ROOT}/clients" export WG_MANAGER_ARCHIVE_DIR="${TEST_ROOT}/archive" export WG_MANAGER_LOCK_FILE="${TEST_ROOT}/manager.lock" begin_test 'known command validates config then reports not implemented' set +e output=$("${PROJECT_DIR}/wg-manager.sh" add 2>&1) status=$? set -e assert_equal "$EXIT_OPERATION" "$status" 'статус команды-заглушки' assert_contains 'пока не реализована' "$output" 'сообщение заглушки' begin_test 'invalid configurations are rejected without execution' marker="${TEST_ROOT}/marker" local invalid_file write_valid_config "${TEST_ROOT}/duplicate.conf" for invalid_file in unknown malformed; do write_valid_config "${TEST_ROOT}/${invalid_file}.conf"; done write_valid_config "${TEST_ROOT}/substitution.conf" SERVER_ENDPOINT "\$(touch ${marker})" write_valid_config "${TEST_ROOT}/backtick.conf" SERVER_ENDPOINT "\`touch ${marker}\`" write_valid_config "${TEST_ROOT}/semicolon.conf" SERVER_ENDPOINT "vpn.example.invalid;touch ${marker}" write_valid_config "${TEST_ROOT}/wildcard.conf" SERVER_ENDPOINT 'vpn*.example.invalid' write_valid_config "${TEST_ROOT}/escape.conf" SERVER_ENDPOINT 'vpn\.example.invalid' write_valid_config "${TEST_ROOT}/bad_ipv4.conf" SERVER_VPN_IP '192.0.999.1' write_valid_config "${TEST_ROOT}/bad_port.conf" SERVER_PORT 0 write_valid_config "${TEST_ROOT}/bad_mode.conf" DEFAULT_CLIENT_MODE unsafe write_valid_config "${TEST_ROOT}/bad_range.conf" RANGE_REVERSED 1 write_valid_config "${TEST_ROOT}/bad_path.conf" WG_CONF relative.conf printf 'WG_INTERFACE="wg1"\n' >>"${TEST_ROOT}/duplicate.conf" printf 'NOT_ALLOWED="x"\n' >>"${TEST_ROOT}/unknown.conf" printf 'BROKEN LINE\n' >>"${TEST_ROOT}/malformed.conf" for invalid_file in duplicate unknown malformed substitution backtick semicolon wildcard escape bad_ipv4 bad_port bad_mode bad_range bad_path; do export WG_MANAGER_CONFIG="${TEST_ROOT}/${invalid_file}.conf" set +e output=$(load_config 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" "отклонение ${invalid_file}" case $invalid_file in duplicate) assert_contains 'Дублирующийся параметр' "$output" 'ветка duplicate' ;; unknown) assert_contains 'Неизвестный параметр' "$output" 'ветка unknown' ;; malformed) assert_contains 'Некорректное присваивание' "$output" 'ветка malformed' ;; substitution|backtick|semicolon|wildcard|escape) assert_contains 'Недопустимый синтаксис' "$output" "ветка ${invalid_file}" ;; bad_ipv4) assert_contains 'Некорректный IPv4' "$output" 'ветка IPv4' ;; bad_port) assert_contains 'Некорректный порт' "$output" 'ветка port' ;; bad_mode) assert_contains 'Некорректный режим' "$output" 'ветка mode' ;; bad_range) assert_contains 'Некорректный диапазон' "$output" 'ветка range' ;; bad_path) assert_contains 'Некорректный путь' "$output" 'ветка path' ;; esac done assert_file_absent "$marker" 'конфигурация не выполняет текст' begin_test 'semantic validation rejects one invalid value at a time' local semantic_case semantic_output begin_test 'IPv6 validator rejects malformed compression and accepts valid forms' for invalid_value in ::: 2001:db8:::1; do set +e validate_ipv6 "$invalid_value" status=$? set -e assert_failure "$status" "отклонён IPv6 ${invalid_value}" done for valid_value in :: 2001:db8::1 2001:db8:0:1:2:3:4:5; do set +e validate_ipv6 "$valid_value" status=$? set -e assert_success "$status" "принят IPv6 ${valid_value}" done declare -a semantic_cases=( 'server_inside|SERVER_VPN_IP|192.0.2.100|Диапазон клиентов содержит' 'empty_dns|CLIENT_DNS||Некорректный список DNS' 'trailing_dns|CLIENT_DNS|192.0.2.53,|Некорректный список DNS' 'dotted_endpoint|SERVER_ENDPOINT|999.999.999.999|Некорректная конечная точка' 'bracketed_hostname|SERVER_ENDPOINT|[vpn.example.invalid]|Некорректная конечная точка' 'invalid_interface|WG_INTERFACE|bad/name|Некорректное имя интерфейса' 'invalid_client_path|CLIENT_DIR|relative|Некорректный путь' 'invalid_dns|CLIENT_DNS|192.0.2.53,,2001:db8::53|Некорректный список DNS' 'invalid_prefix|VPN_SUBNET_PREFIX|192.0|Некорректный префикс VPN' 'mismatched_prefix|VPN_SUBNET_PREFIX|192.0.3|Префикс VPN не совпадает' 'nonnumeric_port|SERVER_PORT|abc|Некорректный порт' 'nonnumeric_range|START_HOST|abc|Некорректный диапазон' 'out_of_range|START_HOST|0|Некорректный диапазон' 'invalid_ipv6|IPV6_PREFIX|gg::1|Некорректный IPv6' 'triple_ipv6_prefix|IPV6_PREFIX|:::|Некорректный IPv6' 'triple_ipv6_dns|CLIENT_DNS|192.0.2.53,:::|Некорректный адрес DNS' 'triple_ipv6_endpoint|SERVER_ENDPOINT|:::|Некорректная конечная точка' 'invalid_ipv6_mode|USE_IPV6|2|USE_IPV6 должен' 'invalid_retention|BACKUP_RETENTION|0|Некорректное число резервных копий' 'invalid_handshake|RECENT_HANDSHAKE_SECONDS|604801|Некорректный срок handshake' ) for semantic_case in "${semantic_cases[@]}"; do IFS='|' read -r invalid_file invalid_key invalid_value expected_text <<<"$semantic_case" write_valid_config "${TEST_ROOT}/${invalid_file}.conf" "$invalid_key" "$invalid_value" WG_MANAGER_CONFIG="${TEST_ROOT}/${invalid_file}.conf" set +e semantic_output=$(load_config 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" "отклонение ${invalid_file}" assert_contains "$expected_text" "$semantic_output" "диагностика ${invalid_file}" done begin_test 'valid endpoint forms and split tunnel are accepted' local valid_case valid_output declare -a valid_cases=( 'documentation_ipv4|SERVER_ENDPOINT|192.0.2.10' 'plain_ipv6|SERVER_ENDPOINT|2001:db8::10' 'bracketed_ipv6|SERVER_ENDPOINT|[2001:db8::10]' 'split_tunnel|DEFAULT_CLIENT_MODE|split-tunnel' ) for valid_case in "${valid_cases[@]}"; do IFS='|' read -r invalid_file invalid_key invalid_value <<<"$valid_case" write_valid_config "${TEST_ROOT}/${invalid_file}.conf" "$invalid_key" "$invalid_value" WG_MANAGER_CONFIG="${TEST_ROOT}/${invalid_file}.conf" set +e valid_output=$(load_config 2>&1) status=$? set -e assert_success "$status" "принимается ${invalid_file}" assert_equal '' "$valid_output" "нет ошибки ${invalid_file}" done begin_test 'runtime path overrides reject unsafe values' WG_MANAGER_CONFIG='relative.conf' set +e set_runtime_paths >/dev/null 2>&1 status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'относительный override отклонён' WG_MANAGER_CONFIG='' set +e set_runtime_paths >/dev/null 2>&1 status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'пустой override отклонён' WG_MANAGER_BACKUP_DIR="${TEST_ROOT}/../escape" set +e set_runtime_paths >/dev/null 2>&1 status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'traversal override отклонён' WG_MANAGER_BACKUP_DIR="${TEST_ROOT}/[unsafe]" set +e set_runtime_paths >/dev/null 2>&1 status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'скобки в override отклонены' write_valid_config "${TEST_ROOT}/bracket-path.conf" WG_CONF "${TEST_ROOT}/[unsafe]" export WG_MANAGER_BACKUP_DIR="${TEST_ROOT}/backups" WG_MANAGER_CONFIG="${TEST_ROOT}/bracket-path.conf" set +e output=$(load_config 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'скобки в пути конфигурации отклонены' assert_contains 'Некорректный путь' "$output" 'диагностика скобок в пути' export WG_MANAGER_CONFIG="$valid_config" } run_gum_tests() { local copied_script="${TEST_ROOT}/manager-copy.sh" output status cp -- "${PROJECT_DIR}/wg-manager.sh" "$copied_script" chmod 700 "$copied_script" mkdir -p "${TEST_ROOT}/bin" "${TEST_ROOT}/empty-path" printf '#!/usr/bin/env bash\nexit 0\n' >"${TEST_ROOT}/override-gum" chmod 700 "${TEST_ROOT}/override-gum" printf '#!/usr/bin/env bash\nexit 0\n' >"${TEST_ROOT}/bin/gum" chmod 700 "${TEST_ROOT}/bin/gum" begin_test 'gum environment override has priority' output=$(WG_MANAGER_GUM="${TEST_ROOT}/override-gum" PATH="${TEST_ROOT}/empty-path" /bin/bash "${TESTS_DIR}/resolve-gum.sh" "$copied_script") assert_equal "${TEST_ROOT}/override-gum" "$output" 'WG_MANAGER_GUM' begin_test 'local gum binary precedes PATH' output=$(env -u WG_MANAGER_GUM PATH="${TEST_ROOT}/empty-path" /bin/bash "${TESTS_DIR}/resolve-gum.sh" "$copied_script") assert_equal "${TEST_ROOT}/bin/gum" "$output" 'локальный gum' begin_test 'PATH gum is used when local binary is absent' rm -- "${TEST_ROOT}/bin/gum" printf '#!/usr/bin/env bash\nexit 0\n' >"${TEST_ROOT}/empty-path/gum" chmod 700 "${TEST_ROOT}/empty-path/gum" output=$(env -u WG_MANAGER_GUM PATH="${TEST_ROOT}/empty-path" /bin/bash "${TESTS_DIR}/resolve-gum.sh" "$copied_script") assert_equal "${TEST_ROOT}/empty-path/gum" "$output" 'gum из PATH' rm -- "${TEST_ROOT}/empty-path/gum" begin_test 'gum completely absent reports dependency failure' set +e output=$(env -u WG_MANAGER_GUM PATH="${TEST_ROOT}/empty-path" /bin/bash "${TESTS_DIR}/resolve-gum.sh" "$copied_script" 2>&1) status=$? set -e assert_equal "$EXIT_DEPENDENCY" "$status" 'gum отсутствует' assert_contains 'Не найден gum' "$output" 'диагностика отсутствующего gum' mkdir -- "${TEST_ROOT}/directory-gum" begin_test 'gum directory override is rejected' set +e output=$(WG_MANAGER_GUM="${TEST_ROOT}/directory-gum" PATH="${TEST_ROOT}/empty-path" /bin/bash "${TESTS_DIR}/resolve-gum.sh" "$copied_script" 2>&1) status=$? set -e assert_equal "$EXIT_DEPENDENCY" "$status" 'каталог WG_MANAGER_GUM' mkdir -- "${TEST_ROOT}/bin/gum" begin_test 'local gum directory is rejected' set +e output=$(env -u WG_MANAGER_GUM PATH="${TEST_ROOT}/empty-path" /bin/bash "${TESTS_DIR}/resolve-gum.sh" "$copied_script" 2>&1) status=$? set -e assert_equal "$EXIT_DEPENDENCY" "$status" 'каталог local gum' begin_test 'unusable gum override fails without fallback' set +e output=$(WG_MANAGER_GUM="${TEST_ROOT}/missing-gum" PATH="${TEST_ROOT}/empty-path" /bin/bash "${TESTS_DIR}/resolve-gum.sh" "$copied_script" 2>&1) status=$? set -e assert_equal "$EXIT_DEPENDENCY" "$status" 'ошибка WG_MANAGER_GUM' assert_contains 'WG_MANAGER_GUM' "$output" 'сообщение WG_MANAGER_GUM' } run_dependency_tests() { local output status code local -a exit_codes=("$EXIT_USAGE" "$EXIT_VALIDATION" "$EXIT_OPERATION" "$EXIT_ROLLBACK" "$EXIT_PERMISSION" "$EXIT_DEPENDENCY") declare -A seen_codes=() begin_test 'stable exit categories are distinct' assert_equal 0 "$EXIT_SUCCESS" 'EXIT_SUCCESS' for code in "${exit_codes[@]}"; do assert_failure "$code" "ненулевой код ${code}" if [[ -v "seen_codes[$code]" ]]; then record_failure "коды не различаются: ${code}" else seen_codes["$code"]=1 ((TEST_ASSERTIONS += 1)) fi done begin_test 'required and optional dependency checks are controlled' output=$(require_command wg) assert_contains '/tests/mocks/bin/wg' "$output" 'обязательная команда через mock PATH' output=$(optional_command gum) assert_contains '/tests/mocks/bin/gum' "$output" 'необязательная команда через mock PATH' set +e require_command definitely-missing >/dev/null 2>&1 status=$? set -e assert_equal "$EXIT_DEPENDENCY" "$status" 'отсутствующая обязательная команда' set +e optional_command definitely-missing >/dev/null 2>&1 status=$? set -e assert_equal "$EXIT_DEPENDENCY" "$status" 'отсутствующая необязательная команда' } run_debug_tests() { local output status begin_test 'disabled debug is a successful no-op' unset WG_MANAGER_DEBUG set +e output=$(debug hidden 2>&1) status=$? set -e assert_success "$status" 'отключённая отладка' assert_equal '' "$output" 'отключённая отладка без вывода' begin_test 'enabled debug uses stderr only' : >"${TEST_ROOT}/debug.stdout" : >"${TEST_ROOT}/debug.stderr" WG_MANAGER_DEBUG=1 debug visible >"${TEST_ROOT}/debug.stdout" 2>"${TEST_ROOT}/debug.stderr" assert_equal '' "$(<"${TEST_ROOT}/debug.stdout")" 'debug stdout пуст' assert_contains 'Отладка: visible' "$(<"${TEST_ROOT}/debug.stderr")" 'debug stderr' } write_m2_config() { local output_file=$1 wireguard_file=$2 write_valid_config "$output_file" replace_config_line "$output_file" 'WG_CONF=' "WG_CONF=\"${wireguard_file}\"" } write_alice_metadata() { mkdir -p "$METADATA_DIR" cat >"${METADATA_DIR}/alice.meta" <"${METADATA_DIR}/bob.meta" <"$TEST_MOCK_LOG" unset WG_MOCK_DUMP_FILE WG_MOCK_INTERFACE begin_test 'persistent WireGuard parser and trusted metadata identity' write_alice_metadata parse_wireguard_config "$fixture" assert_equal 1 "${#DISK_ALLOWED[@]}" 'один peer на диске' assert_equal 192.0.2.2/32 "${DISK_IPV4[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'адрес peer' parse_metadata assert_equal alice "${META_NAME[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'имя из метаданных' assert_equal 1 "${META_MANAGED[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'доверенная метка managed' assert_equal "${CLIENT_DIR}/alice.conf" "${META_CONFIG[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'путь клиентского конфига' assert_equal 1700000000 "${META_CREATED[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'время создания метаданных' begin_test 'two managed peers match exact keys and addresses' cp -- "${PROJECT_DIR}/tests/fixtures/m2/multiple-managed.conf" "$fixture" write_bob_metadata printf 'IPV6=2001:db8::2\n' >>"${METADATA_DIR}/alice.meta" load_client_model local managed_count=0 for record_index in "${!CLIENT_KEYS[@]}"; do [[ ${CLIENT_ORIGINS[$record_index]} == managed ]] && ((managed_count += 1)); done assert_equal 2 "$managed_count" 'две управляемые записи' rm -- "${METADATA_DIR}/bob.meta" cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture" begin_test 'pre-peer marker and spaced mixed AllowedIPs are parsed' cp -- "${PROJECT_DIR}/tests/fixtures/m2/pre-peer-marker.conf" "$fixture" parse_wireguard_config "$fixture" assert_equal alice "${DISK_MARKER[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'маркер перед peer' assert_equal '192.0.2.2/32,2001:db8::2/128' "${DISK_ALLOWED[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'нормализация AllowedIPs' cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture" begin_test 'repeatable interface fields and duplicate peer singleton checks' cp -- "${PROJECT_DIR}/tests/fixtures/m2/repeatable-interface.conf" "$fixture" parse_wireguard_config "$fixture" replace_config_line "$fixture" 'FwMark =' 'FwMark = 0xca6c' parse_wireguard_config "$fixture" for bad_fwmark in 0x100000000 0xnope 4294967296; do replace_config_line "$fixture" 'FwMark =' "FwMark = ${bad_fwmark}" set +e parse_wireguard_config "$fixture" >/dev/null 2>&1 status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" "некорректный FwMark ${bad_fwmark}" done cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture" printf 'Endpoint = vpn.example.invalid:51820\n' >>"$fixture" set +e output=$(parse_wireguard_config "$fixture" 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'дублирующийся Endpoint' assert_contains 'Дублирующийся Endpoint' "$output" 'диагностика duplicate Endpoint' for duplicate_field in PresharedKey AllowedIPs PersistentKeepalive; do cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture" case $duplicate_field in PresharedKey) printf 'PresharedKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\nPresharedKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n' >>"$fixture" ;; AllowedIPs) printf 'AllowedIPs = 192.0.2.2/32\nAllowedIPs = 192.0.2.2/32\n' >>"$fixture" ;; PersistentKeepalive) printf 'PersistentKeepalive = 25\nPersistentKeepalive = 25\n' >>"$fixture" ;; esac set +e output=$(parse_wireguard_config "$fixture" 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" "дубликат ${duplicate_field}" done cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture" printf 'Endpoint = vpn.example.invalid\n' >>"$fixture" set +e output=$(parse_wireguard_config "$fixture" 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'Endpoint без порта' cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture" printf 'PresharedKey = (none)\n' >>"$fixture" set +e output=$(parse_wireguard_config "$fixture" 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'persistent PSK token отвергнут' begin_test 'section ordering and marker leakage are rejected safely' printf '# wg-manager-client: leaked\n[Interface]\nPrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\nListenPort = 51820\n[Peer]\nPublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\nAllowedIPs = 192.0.2.2/32\n' >"${TEST_ROOT}/marker-leak.conf" parse_wireguard_config "${TEST_ROOT}/marker-leak.conf" assert_equal '' "${DISK_MARKER[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]:-}" 'маркер не протёк через Interface' for invalid_section in peer-before-interface empty-config repeated-interface; do case $invalid_section in peer-before-interface) printf '[Peer]\nPublicKey = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\nAllowedIPs = 192.0.2.2/32\n' >"${TEST_ROOT}/${invalid_section}.conf" ;; empty-config) : >"${TEST_ROOT}/${invalid_section}.conf" ;; repeated-interface) printf '[Interface]\nPrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n[Interface]\nListenPort = 51820\n' >"${TEST_ROOT}/${invalid_section}.conf" ;; esac set +e parse_wireguard_config "${TEST_ROOT}/${invalid_section}.conf" >/dev/null 2>&1 status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" "секция ${invalid_section}" done begin_test 'adjacent conventional markers attach to the next peer' cp -- "${PROJECT_DIR}/tests/fixtures/m2/markers-adjacent.conf" "$fixture" parse_wireguard_config "$fixture" assert_equal first "${DISK_LEGACY[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'первый marker' assert_equal second "${DISK_LEGACY[CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=]}" 'соседний marker' assert_equal third "${DISK_MARKER[DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=]}" 'соседний explicit marker' begin_test 'metadata names remain restricted and cannot create paths' rm -- "${METADATA_DIR}/alice.meta" local unsafe_name metadata_output for unsafe_name in 'bad name' '*glob?' '-name' $'bad\nname'; do printf 'NAME=%s\nPUBLIC_KEY=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\nIPV4=192.0.2.2\nCREATED_AT=1700000000\nCONFIG_PATH=%s/bad.conf\nCLIENT_MODE=full-tunnel\nMANAGED=1\n' "$unsafe_name" "$CLIENT_DIR" >"${METADATA_DIR}/bad.meta" set +e metadata_output=$(parse_metadata 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" "unsafe metadata name ${unsafe_name@Q}" assert_equal '' "$(printf '%s' "$metadata_output" | grep -E 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=|PrivateKey|PresharedKey' || true)" 'metadata error without key material' assert_equal 0 "$( [[ ! -e "${CLIENT_DIR}/${unsafe_name}.conf" ]] && printf 0 || printf 1 )" 'unsafe metadata does not create path' rm -- "${METADATA_DIR}/bad.meta" done write_alice_metadata begin_test 'duplicate metadata identity is rejected' cp -- "${METADATA_DIR}/alice.meta" "${METADATA_DIR}/duplicate.meta" set +e output=$(parse_metadata 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'дубликат метаданных' assert_contains 'Дублирующийся' "$output" 'диагностика дубликата метаданных' rm -- "${METADATA_DIR}/duplicate.meta" begin_test 'empty peer list remains a valid disk model' cp -- "${PROJECT_DIR}/tests/fixtures/m2/no-peers.conf" "$fixture" rm -- "${METADATA_DIR}/alice.meta" unset WG_MOCK_DUMP_FILE load_client_model assert_equal 0 "${#CLIENT_KEYS[@]}" 'нет peers' write_alice_metadata cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture" begin_test 'metadata address mismatch never establishes managed identity' replace_config_line "${METADATA_DIR}/alice.meta" 'IPV4=' 'IPV4=192.0.2.99' load_client_model assert_equal legacy "${CLIENT_ORIGINS[0]}" 'mismatch origin' write_alice_metadata begin_test 'legacy and unmanaged origins remain distinct from managed identity' cp -- "${PROJECT_DIR}/tests/fixtures/m2/multiple.conf" "$fixture" rm -- "${METADATA_DIR}/alice.meta" unset WG_MOCK_DUMP_FILE load_client_model local record_index for record_index in "${!CLIENT_KEYS[@]}"; do case ${CLIENT_KEYS[$record_index]} in BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=) assert_equal legacy "${CLIENT_ORIGINS[$record_index]}" 'peer с явным marker без метаданных'; assert_equal unmanaged "${CLIENT_STATES[$record_index]}" 'marker без доверенной метадаты' ;; CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=) assert_equal legacy "${CLIENT_ORIGINS[$record_index]}" 'legacy peer'; assert_equal unmanaged "${CLIENT_STATES[$record_index]}" 'legacy state' ;; DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=) assert_equal unmanaged "${CLIENT_ORIGINS[$record_index]}" 'unmanaged peer' ;; esac done begin_test 'runtime parser and exact wg dump command' cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture" write_alice_metadata cp -- "${PROJECT_DIR}/tests/fixtures/m2/runtime.dump" "$runtime_file" export WG_MOCK_DUMP_FILE="$runtime_file" WG_MOCK_INTERFACE=wg0 : >"$TEST_MOCK_LOG" load_client_model assert_equal 1 "$RUNTIME_AVAILABLE" 'runtime доступен' assert_equal 51820 "$RUNTIME_LISTEN_PORT" 'порт runtime' assert_equal active "${CLIENT_STATES[0]}" 'активный peer на границе threshold' local runtime_log runtime_log=$(<"$TEST_MOCK_LOG") runtime_log+=$'\n' assert_equal $'wg show wg0 dump\n' "$runtime_log" 'точный argv wg' assert_equal '' "$(printf '%s' "$runtime_log" | grep -E 'systemctl|ip|gum' || true)" 'runtime без лишних команд' begin_test 'runtime unavailable tokens normalize safely' parse_runtime_dump $'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\t(none)\t(none)\t0\t0\t0\t0' assert_equal - "${RUNTIME_ENDPOINT[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'endpoint none' assert_equal - "${RUNTIME_ALLOWED[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'allowed none' parse_runtime_dump $'(none)\t(none)\t51820\t0' assert_equal 51820 "$RUNTIME_LISTEN_PORT" 'interface none tokens' set +e output=$(parse_runtime_dump $'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\tvpn.example.invalid\t192.0.2.2/32\t0\t0\t0\t0' 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'runtime endpoint without port' parse_runtime_dump $'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\t[2001:db8::1]:51820\t192.0.2.2/32\t0\t0\t0\t0' assert_equal '[2001:db8::1]:51820' "${RUNTIME_ENDPOINT[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'runtime IPv6 endpoint' begin_test 'unavailable runtime command degrades safely' local saved_path=$PATH local safe_runtime_path="${TEST_ROOT}/no-wg" mkdir -p "$safe_runtime_path" ln -s -- "$(type -P sort)" "${safe_runtime_path}/sort" ln -s -- "$(type -P paste)" "${safe_runtime_path}/paste" PATH=$safe_runtime_path if command -v wg >/dev/null 2>&1; then record_failure 'контролируемый PATH неожиданно содержит wg' fi unset WG_MOCK_DUMP_FILE output=$(/bin/bash "${PROJECT_DIR}/wg-manager.sh" status) PATH=$saved_path assert_contains 'недоступен' "$output" 'wg отсутствует' export WG_MOCK_DUMP_FILE="$runtime_file" WG_MOCK_INTERFACE=wg0 begin_test 'active threshold, inactive, and never states are deterministic' replace_config_line "$runtime_file" 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=' $'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tvpn.example.invalid:51820\t192.0.2.2/32\t1699999919\t100\t200\t25' load_client_model assert_equal inactive "${CLIENT_STATES[0]}" 'peer just beyond threshold' replace_config_line "$runtime_file" 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=' $'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tvpn.example.invalid:51820\t192.0.2.2/32\t0\t100\t200\t25' load_client_model assert_equal never "${CLIENT_STATES[0]}" 'peer without handshake' cp -- "${PROJECT_DIR}/tests/fixtures/m2/runtime.dump" "$runtime_file" begin_test 'runtime-only peer is retained as unmanaged' printf '%s\t%s\t-\t192.0.2.9/32\t1700000100\t1\t2\t0\n' 'EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=' '(none)' >>"$runtime_file" load_client_model local runtime_index runtime_origin='' assert_contains EEEEEEEE "$(printf '%s\n' "${CLIENT_KEYS[@]}")" 'runtime-only key parsed' for runtime_index in "${!CLIENT_KEYS[@]}"; do [[ ${CLIENT_KEYS[$runtime_index]} == EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE= ]] && runtime_origin=${CLIENT_ORIGINS[$runtime_index]} done assert_equal unmanaged "$runtime_origin" 'runtime-only origin' begin_test 'malformed runtime dump fails without exposing fields' printf 'malformed runtime line\n' >"$runtime_file" set +e output=$("${PROJECT_DIR}/wg-manager.sh" list 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'malformed runtime status' assert_contains 'wg dump' "$output" 'malformed runtime diagnostic' cp -- "${PROJECT_DIR}/tests/fixtures/m2/runtime.dump" "$runtime_file" begin_test 'status is useful without gum or systemd' local model_before model_after model_before=$(snapshot_model_inputs "$config_file" "$fixture" "$METADATA_DIR" "$runtime_file" "$BACKUP_DIR" "$LOG_DIR" "$ARCHIVE_DIR" "$LOCK_FILE") output=$("${PROJECT_DIR}/wg-manager.sh" status 2>&1) assert_contains 'Интерфейс: wg0' "$output" 'status interface' assert_contains 'Управляемые: 1' "$output" 'status managed count' assert_contains 'Активные: 1' "$output" 'status active count' output=$("${PROJECT_DIR}/wg-manager.sh" list) model_after=$(snapshot_model_inputs "$config_file" "$fixture" "$METADATA_DIR" "$runtime_file" "$BACKUP_DIR" "$LOG_DIR" "$ARCHIVE_DIR" "$LOCK_FILE") assert_equal "$model_before" "$model_after" 'status/list не изменяют состояние' begin_test 'invalid deterministic clock fails without secrets' WG_MANAGER_NOW=invalid set +e output=$("${PROJECT_DIR}/wg-manager.sh" status 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'некорректные часы' assert_equal '' "$(printf '%s' "$output" | grep -E 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=|BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=' || true)" 'ошибка часов без ключей' WG_MANAGER_NOW=1700000100 begin_test 'list is deterministic and secret-free' output=$("${PROJECT_DIR}/wg-manager.sh" list) assert_contains $'NAME\tADDRESSES\tPUBLIC_KEY' "$output" 'list header' assert_contains $'alice\t192.0.2.2/32\tBBBBBBBB' "$output" 'list managed row' assert_equal '' "$(grep -E 'PrivateKey|PresharedKey|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' <<<"$output" || true)" 'list не показывает секретные поля' begin_test 'redirected list remains plain text' "${PROJECT_DIR}/wg-manager.sh" list >"${TEST_ROOT}/list.out" assert_contains $'alice\t192.0.2.2/32' "$(<"${TEST_ROOT}/list.out")" 'redirected list row' assert_equal '' "$(LC_ALL=C grep -nP '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]' "${TEST_ROOT}/list.out" || true)" 'redirected list without controls' begin_test 'list ordering is independent of peer input order' rm -- "${METADATA_DIR}/alice.meta" unset WG_MOCK_DUMP_FILE cp -- "${PROJECT_DIR}/tests/fixtures/m2/multiple.conf" "$fixture" output=$("${PROJECT_DIR}/wg-manager.sh" list) cp -- "${PROJECT_DIR}/tests/fixtures/m2/multiple-reversed.conf" "$fixture" assert_equal "$output" "$("${PROJECT_DIR}/wg-manager.sh" list)" 'стабильная сортировка list' cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture" write_alice_metadata export WG_MOCK_DUMP_FILE="$runtime_file" WG_MOCK_INTERFACE=wg0 begin_test 'stopped runtime degrades to disk-only output' unset WG_MOCK_DUMP_FILE output=$("${PROJECT_DIR}/wg-manager.sh" status) assert_contains 'недоступен' "$output" 'runtime unavailable' assert_contains 'Peers: 1' "$output" 'disk peer count' begin_test 'malformed persistent config fails safely' cp -- "${PROJECT_DIR}/tests/fixtures/m2/malformed.conf" "$fixture" set +e output=$("${PROJECT_DIR}/wg-manager.sh" list 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'malformed config status' assert_contains 'PublicKey' "$output" 'malformed config diagnostic' begin_test 'duplicate AllowedIPs are rejected' cp -- "${PROJECT_DIR}/tests/fixtures/m2/duplicate-allowed.conf" "$fixture" set +e output=$("${PROJECT_DIR}/wg-manager.sh" list 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'duplicate AllowedIPs status' assert_contains 'AllowedIPs' "$output" 'duplicate AllowedIPs diagnostic' begin_test 'duplicate public keys and overlapping networks are rejected' for invalid_fixture in duplicate-public overlap overlap-v6; do cp -- "${PROJECT_DIR}/tests/fixtures/m2/${invalid_fixture}.conf" "$fixture" set +e output=$("${PROJECT_DIR}/wg-manager.sh" list 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" "${invalid_fixture} status" if [[ $invalid_fixture == duplicate-public ]]; then assert_contains 'PublicKey' "$output" "${invalid_fixture} diagnostic"; else assert_contains 'AllowedIPs' "$output" "${invalid_fixture} diagnostic"; fi done } run_m2_focused() { local group=$1 config_file="${TEST_ROOT}/focused-m2.conf" fixture="${TEST_ROOT}/wg0.conf" runtime_file="${TEST_ROOT}/runtime.dump" output reversed_output status cp -- "${PROJECT_DIR}/tests/fixtures/m2/managed.conf" "$fixture" write_m2_config "$config_file" "$fixture" export WG_MANAGER_CONFIG="$config_file" WG_MANAGER_BACKUP_DIR="${TEST_ROOT}/backups" WG_MANAGER_LOG_DIR="${TEST_ROOT}/logs" WG_MANAGER_METADATA_DIR="${TEST_ROOT}/clients" WG_MANAGER_ARCHIVE_DIR="${TEST_ROOT}/archive" WG_MANAGER_LOCK_FILE="${TEST_ROOT}/lock" WG_MANAGER_NOW=1700000100 set_runtime_paths WG_CONF=$fixture mkdir -p "$METADATA_DIR" load_config write_alice_metadata case $group in parser) begin_test 'focused persistent and runtime parser' parse_wireguard_config "$fixture" parse_metadata cp -- "${PROJECT_DIR}/tests/fixtures/m2/repeatable-interface.conf" "$fixture" replace_config_line "$fixture" 'FwMark =' 'FwMark = 0xca6c' parse_wireguard_config "$fixture" assert_equal 1 "${#DISK_ALLOWED[@]}" 'hex FwMark не ломает parser' cp -- "${PROJECT_DIR}/tests/fixtures/m2/markers-adjacent.conf" "$fixture" parse_wireguard_config "$fixture" assert_equal second "${DISK_LEGACY[CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=]}" 'focused marker scoping' cp -- "${PROJECT_DIR}/tests/fixtures/m2/runtime.dump" "$runtime_file" parse_runtime_dump "$(<"$runtime_file")" assert_equal 1 "$RUNTIME_AVAILABLE" 'runtime parser доступен' assert_equal 51820 "$RUNTIME_LISTEN_PORT" 'порт интерфейса runtime' assert_equal 1700000100 "${RUNTIME_HANDSHAKE[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'handshake peer' assert_equal 100 "${RUNTIME_RX[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'rx peer' assert_equal 200 "${RUNTIME_TX[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'tx peer' assert_equal '192.0.2.2/32' "${RUNTIME_ALLOWED[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'allowed ips peer' parse_runtime_dump $'(none)\t(none)\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\t(none)\t[2001:db8::1]:51820\t192.0.2.2/32\t0\t0\t0\t0' assert_equal '[2001:db8::1]:51820' "${RUNTIME_ENDPOINT[BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=]}" 'focused runtime endpoint' set +e output=$(parse_runtime_dump $'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\t51820\t0\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\tmalformed\t(none)\t(none)\t0\t0\t0\t0' 2>&1) status=$? set -e assert_equal "$EXIT_VALIDATION" "$status" 'malformed PSK отвергнут' assert_contains 'ключ' "$output" 'диагностика malformed PSK' ;; status) begin_test 'focused status output' cp -- "${PROJECT_DIR}/tests/fixtures/m2/runtime.dump" "$runtime_file" export WG_MOCK_DUMP_FILE="$runtime_file" WG_MOCK_INTERFACE=wg0 output=$("${PROJECT_DIR}/wg-manager.sh" status) assert_contains 'Интерфейс: wg0' "$output" 'status interface' assert_contains 'Управляемые: 1' "$output" 'status managed' ;; list) begin_test 'focused list output' cp -- "${PROJECT_DIR}/tests/fixtures/m2/runtime.dump" "$runtime_file" export WG_MOCK_DUMP_FILE="$runtime_file" WG_MOCK_INTERFACE=wg0 output=$("${PROJECT_DIR}/wg-manager.sh" list) assert_contains $'alice\t192.0.2.2/32' "$output" 'list row' assert_equal '' "$(printf '%s' "$output" | grep -E 'PrivateKey|PresharedKey' || true)" 'list secret-free' ;; list-redirected) begin_test 'focused redirected list' cp -- "${PROJECT_DIR}/tests/fixtures/m2/runtime.dump" "$runtime_file" export WG_MOCK_DUMP_FILE="$runtime_file" WG_MOCK_INTERFACE=wg0 "${PROJECT_DIR}/wg-manager.sh" list >"${TEST_ROOT}/focused-list.out" 2>"${TEST_ROOT}/focused-list.err" assert_contains $'alice\t192.0.2.2/32' "$(<"${TEST_ROOT}/focused-list.out")" 'redirected row' assert_equal '' "$(<"${TEST_ROOT}/focused-list.err")" 'redirected stderr empty' ;; list-deterministic) begin_test 'focused deterministic list ordering' unset WG_MOCK_DUMP_FILE cp -- "${PROJECT_DIR}/tests/fixtures/m2/multiple.conf" "$fixture" output=$("${PROJECT_DIR}/wg-manager.sh" list) cp -- "${PROJECT_DIR}/tests/fixtures/m2/multiple-reversed.conf" "$fixture" reversed_output=$("${PROJECT_DIR}/wg-manager.sh" list) assert_equal "$output" "$reversed_output" 'порядок list стабилен' ;; esac } if [[ -z $test_group ]]; then begin_test 'sourcing entry point does not execute main' source_output='' source_output=$(/bin/bash "${TESTS_DIR}/source-entry.sh" "${PROJECT_DIR}/wg-manager.sh") assert_equal sourced "$source_output" 'безопасная загрузка' begin_test 'help succeeds without root or dependencies' set +e help_output=$("${PROJECT_DIR}/wg-manager.sh" help 2>&1) help_status=$? set -e assert_success "$help_status" 'команда help' assert_contains 'Использование:' "$help_output" 'справка' assert_equal "$help_output" "${help_output//$'\e'/}" 'help без ANSI' begin_test 'version succeeds without config or dependencies' set +e version_output=$(env -u WG_MANAGER_CONFIG -u WG_MANAGER_GUM PATH=/usr/bin:/bin "${PROJECT_DIR}/wg-manager.sh" version 2>&1) version_status=$? set -e assert_success "$version_status" 'команда version' assert_contains 'WireGuard Manager 0.1.0' "$version_output" 'версия' assert_equal "$version_output" "${version_output//$'\e'/}" 'version без ANSI' begin_test 'extra arguments fail with usage status' set +e extra_output=$("${PROJECT_DIR}/wg-manager.sh" help extra 2>&1) extra_status=$? set -e assert_equal "$EXIT_USAGE" "$extra_status" 'лишний аргумент' assert_contains 'Ожидается одна команда' "$extra_output" 'диагностика лишнего аргумента' begin_test 'unknown command fails' set +e unknown_output=$("${PROJECT_DIR}/wg-manager.sh" unknown 2>&1) unknown_status=$? set -e assert_failure "$unknown_status" 'неизвестная команда' assert_contains 'неизвестная команда' "$unknown_output" 'сообщение об ошибке' begin_test 'help works outside project directory' set +e outside_output=$(cd /tmp && "${PROJECT_DIR}/wg-manager.sh" help 2>&1) outside_status=$? set -e assert_success "$outside_status" 'запуск из /tmp' assert_contains 'Использование:' "$outside_output" 'справка из /tmp' begin_test 'mock guard accepts only explicitly supported empty invocation' set +e MOCK_ALLOW_EMPTY_INVOCATION=wg wg expected_mock_status=$? wg unapproved_empty_status=$? wg unexpected unexpected_mock_status=$? set -e assert_success "$expected_mock_status" 'разрешённый вызов мока' assert_failure "$unapproved_empty_status" 'неразрешённый пустой вызов мока' assert_failure "$unexpected_mock_status" 'неожиданный вызов мока' expected_mock_log=$'wg\nwg\nwg unexpected\n' actual_mock_log=$(<"$TEST_MOCK_LOG") actual_mock_log+=$'\n' assert_equal "$expected_mock_log" "$actual_mock_log" 'точный журнал вызовов мока' begin_test 'test writes stay below isolated root' assert_file_exists "${TEST_ROOT}/wg0.conf" 'копия фикстуры' assert_equal "$TEST_ROOT" "${TEST_MOCK_LOG%/*}" 'журнал находится в тестовом корне' scan_test_sources_for_host_path() { local forbidden_path='/etc' forbidden_path+='/wireguard' scan_path() { local path line for path in "$1"/*; do if [[ -d "$path" ]]; then scan_path "$path" elif [[ -f "$path" ]]; then while IFS= read -r line || [[ -n "$line" ]]; do if [[ "$line" == *"$forbidden_path"* ]]; then record_failure "тестовый файл содержит запрещённый путь: ${path}" return fi done <"$path" fi done } scan_path "$TESTS_DIR" } scan_sources_for_absolute_external_commands() { local command_name candidate path line for command_name in wg wg-quick ip systemctl nft iptables journalctl sysctl; do candidate='/bin' candidate+="/${command_name}" for path in "$PROJECT_DIR/wg-manager.sh" "$TESTS_DIR"/*.sh "$TESTS_DIR"/mocks/bin/*; do [[ -f "$path" ]] || continue while IFS= read -r line || [[ -n "$line" ]]; do if [[ "$line" == "$candidate" || "$line" == *" $candidate"* || "$line" == *"\"$candidate"* || "$line" == *"'$candidate"* || "$line" == *"=$candidate"* ]]; then record_failure "абсолютный внешний вызов в ${path}: ${candidate}"; break; fi done <"$path" done candidate='/usr' candidate+="/bin/${command_name}" for path in "$PROJECT_DIR/wg-manager.sh" "$TESTS_DIR"/*.sh "$TESTS_DIR"/mocks/bin/*; do [[ -f "$path" ]] || continue while IFS= read -r line || [[ -n "$line" ]]; do if [[ "$line" == "$candidate" || "$line" == *" $candidate"* || "$line" == *"\"$candidate"* || "$line" == *"'$candidate"* || "$line" == *"=$candidate"* ]]; then record_failure "абсолютный внешний вызов в ${path}: ${candidate}"; break; fi done <"$path" done done } scan_test_sources_for_host_path scan_sources_for_absolute_external_commands fi if [[ $test_group == config ]]; then run_config_tests elif [[ $test_group == parser || $test_group == status || $test_group == list || $test_group == list-redirected || $test_group == list-deterministic ]]; then run_m2_focused "$test_group" else run_config_tests run_gum_tests run_dependency_tests run_debug_tests run_m2_tests fi finish_tests