feat(tui): add gum dashboard and read-only workflow
This commit is contained in:
+186
-1
@@ -11,7 +11,7 @@ source "${PROJECT_DIR}/wg-manager.sh"
|
||||
|
||||
case $# in
|
||||
0) ;;
|
||||
1) case $1 in config|parser|status|list|list-redirected|list-deterministic|transaction|rollback|locking|backups|mutation-preserves-source|secret-modes|add-client|address-allocation|add-collisions|add-rollback|no-secret-logs|add-golden-config|add-invalid-names|show-client|config-export|qr|path-safety|no-secret-default-output|remove-client|remove-cancel|remove-exact-block|remove-rollback|archive-client|archive-path-safety|remove-preserves-neighbors|diagnostics|diagnostics-systemd|diagnostics-firewall|diagnostics-forwarding|diagnostics-runtime-diff|diagnostics-read-only|diagnostics-exit-codes|audit|audit-no-secrets|backup-list|restore|restore-invalid|restore-rollback|backup-path-safety|runtime-file-modes) ;; *) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;; esac ;;
|
||||
1) case $1 in config|parser|status|list|list-redirected|list-deterministic|transaction|rollback|locking|backups|mutation-preserves-source|secret-modes|add-client|address-allocation|add-collisions|add-rollback|no-secret-logs|add-golden-config|add-invalid-names|show-client|config-export|qr|path-safety|no-secret-default-output|remove-client|remove-cancel|remove-exact-block|remove-rollback|archive-client|archive-path-safety|remove-preserves-neighbors|diagnostics|diagnostics-systemd|diagnostics-firewall|diagnostics-forwarding|diagnostics-runtime-diff|diagnostics-read-only|diagnostics-exit-codes|audit|audit-no-secrets|backup-list|restore|restore-invalid|restore-rollback|backup-path-safety|runtime-file-modes|gum-resolution|tui-routing|tui-cancel|tui-read-only|tui-no-gum|tui-non-tty) ;; *) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;; esac ;;
|
||||
*) printf 'Ошибка: неизвестная группа тестов или лишние аргументы\n' >&2; exit 64 ;;
|
||||
esac
|
||||
test_group=${1-}
|
||||
@@ -385,6 +385,184 @@ run_gum_tests() {
|
||||
set -e
|
||||
assert_equal "$EXIT_DEPENDENCY" "$status" 'ошибка WG_MANAGER_GUM'
|
||||
assert_contains 'WG_MANAGER_GUM' "$output" 'сообщение WG_MANAGER_GUM'
|
||||
rmdir -- "${TEST_ROOT}/bin/gum"
|
||||
rmdir -- "${TEST_ROOT}/directory-gum"
|
||||
}
|
||||
|
||||
prepare_m9_fixture() {
|
||||
local config_file="${TEST_ROOT}/m9.conf" fixture_error
|
||||
mkdir -p -- "${TEST_ROOT}/clients" "${TEST_ROOT}/metadata" "${TEST_ROOT}/backups" "${TEST_ROOT}/logs"
|
||||
cp -- "${PROJECT_DIR}/tests/fixtures/wg0.conf" "${TEST_ROOT}/wg0.conf"
|
||||
write_valid_config "$config_file"
|
||||
export WG_MANAGER_CONFIG="$config_file" WG_MANAGER_BACKUP_DIR="${TEST_ROOT}/backups" WG_MANAGER_METADATA_DIR="${TEST_ROOT}/metadata" WG_MANAGER_LOG_DIR="${TEST_ROOT}/logs" WG_MANAGER_ARCHIVE_DIR="${TEST_ROOT}/archive" WG_MANAGER_LOCK_FILE="${TEST_ROOT}/manager.lock" WG_MANAGER_GUM="${PROJECT_DIR}/tests/mocks/bin/gum" WG_MANAGER_TEST_MODE=1 WG_MANAGER_TEST_TTY=both GUM_STYLE_CAPTURE="${TEST_ROOT}/gum-style.txt" GUM_PAGER_CAPTURE="${TEST_ROOT}/gum-pager.txt" GUM_CHOICES_CAPTURE="${TEST_ROOT}/gum-choices.txt"
|
||||
mkdir -p -- "${TEST_ROOT}/archive"
|
||||
printf '1\n' >"${TEST_ROOT}/ipv4-forward"
|
||||
printf '1\n' >"${TEST_ROOT}/ipv6-forward"
|
||||
export WG_MANAGER_IPV4_FORWARD_FILE="${TEST_ROOT}/ipv4-forward" WG_MANAGER_IPV6_FORWARD_FILE="${TEST_ROOT}/ipv6-forward"
|
||||
printf '[Interface]\nPrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\nAddress = 10.0.0.2/32\n' >"${TEST_ROOT}/clients/alice.conf"
|
||||
chmod 600 -- "${TEST_ROOT}/clients/alice.conf"
|
||||
printf 'NAME=alice\nPUBLIC_KEY=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\nIPV4=192.0.2.2\nCREATED_AT=1700000000\nCONFIG_PATH=%s\nCLIENT_MODE=full-tunnel\nMANAGED=1\n' "${TEST_ROOT}/clients/alice.conf" >"${TEST_ROOT}/metadata/alice.meta"
|
||||
chmod 600 -- "${TEST_ROOT}/metadata/alice.meta"
|
||||
load_config >/dev/null
|
||||
printf 'timestamp=2023-11-14T22:13:20Z actor_uid=%s operation=tui target=fixture client=fixture key_suffix= outcome=success error=read_only\n' "$EUID" >"${TEST_ROOT}/logs/wg-manager.log"
|
||||
chmod 600 -- "${TEST_ROOT}/logs/wg-manager.log"
|
||||
if ! fixture_error=$(create_backup "$WG_CONF" tui fixture 2>&1); then
|
||||
printf 'Ошибка тестовой фикстуры: не удалось создать резервную копию: %s\n' "$fixture_error" >&2
|
||||
return 1
|
||||
fi
|
||||
: >"$TEST_MOCK_LOG"
|
||||
}
|
||||
|
||||
run_m9_tests() {
|
||||
local output status script_file tty_mode pager_count snapshot_before lock_status long_name
|
||||
case $test_group in
|
||||
gum-resolution)
|
||||
run_gum_tests
|
||||
prepare_m9_fixture
|
||||
begin_test 'gum capability probes accept the scripted mock'
|
||||
output=$(resolve_gum)
|
||||
assert_contains 'tests/mocks/bin/gum' "$output" 'gum mock resolved'
|
||||
validate_gum_capabilities "$output" >/dev/null
|
||||
assert_success "$?" 'gum capabilities'
|
||||
export GUM_FAIL_CAPABILITY=filter
|
||||
set +e; output=$(validate_gum_capabilities "$output" 2>&1); status=$?; set -e
|
||||
assert_equal "$EXIT_DEPENDENCY" "$status" 'missing gum capability status'
|
||||
assert_contains 'filter' "$output" 'missing capability diagnostic names subcommand'
|
||||
unset GUM_FAIL_CAPABILITY
|
||||
printf 'Выбор\nКлиент\ny\ny\n' >"${TEST_ROOT}/gum-wrapper-script"
|
||||
export GUM_SCRIPT_FILE="${TEST_ROOT}/gum-wrapper-script"
|
||||
: >"$TEST_MOCK_LOG"
|
||||
gum_header 'Заголовок' >/dev/null
|
||||
gum_choose 'Клиент' 'Выход' >/dev/null
|
||||
gum_filter 'Клиент' >/dev/null
|
||||
gum_input --placeholder 'Имя' >/dev/null
|
||||
gum_confirm 'Продолжить' >/dev/null
|
||||
printf 'таблица\n' | gum_table >/dev/null
|
||||
printf 'страница\n' | gum_pager >/dev/null
|
||||
gum_spin --title 'Ожидание' >/dev/null
|
||||
begin_test 'all gum wrappers keep their raw invocation details isolated'
|
||||
for capability in style choose filter input confirm table pager spin; do
|
||||
assert_contains "gum $capability" "$(<"$TEST_MOCK_LOG")" "обёртка gum $capability"
|
||||
done
|
||||
;;
|
||||
tui-routing)
|
||||
prepare_m9_fixture
|
||||
script_file="${TEST_ROOT}/gum-script"; printf 'Состояние\nВыход\n' >"$script_file"; export GUM_SCRIPT_FILE="$script_file"
|
||||
begin_test 'TUI routes status through the dashboard loop'
|
||||
set +e; output=$(tui_screen 2>&1); status=$?; set -e
|
||||
assert_equal 0 "$status" 'TUI status route exits cleanly'
|
||||
assert_contains 'gum choose' "$(<"$TEST_MOCK_LOG")" 'menu wrapper invocation recorded'
|
||||
assert_contains 'gum pager' "$(<"$TEST_MOCK_LOG")" 'pager wrapper invocation recorded'
|
||||
assert_contains 'WireGuard Manager — wg0' "$(<"$GUM_STYLE_CAPTURE")" 'dashboard interface'
|
||||
assert_contains 'Состояние:' "$(<"$GUM_STYLE_CAPTURE")" 'dashboard runtime state'
|
||||
assert_contains 'Управляемых:' "$(<"$GUM_STYLE_CAPTURE")" 'dashboard managed count'
|
||||
assert_contains 'Активны недавно:' "$(<"$GUM_STYLE_CAPTURE")" 'dashboard active count'
|
||||
assert_contains 'Предупреждения:' "$(<"$GUM_STYLE_CAPTURE")" 'dashboard warning count'
|
||||
assert_equal 0 "$(grep -c '\\\\n' "$GUM_STYLE_CAPTURE" || true)" 'dashboard has real line breaks'
|
||||
assert_equal 0 "$(awk 'length($0) > 80 { count += 1 } END { print count + 0 }' "$GUM_STYLE_CAPTURE")" 'dashboard lines fit 80 columns'
|
||||
printf 'Неизвестно\nНеизвестно\nНеизвестно\n' >"$script_file"
|
||||
set +e; tui_screen >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal "$EXIT_OPERATION" "$status" 'three route failures are bounded'
|
||||
printf 'Диагностика\nЖурнал\nРезервные копии\nДобавить\nУдалить\nВыход\n' >"$script_file"
|
||||
: >"$TEST_MOCK_LOG"
|
||||
set +e; output=$(tui_screen 2>&1); status=$?; set -e
|
||||
assert_equal 0 "$status" 'all read-only and placeholder routes return to menu'
|
||||
assert_contains 'Действие «Добавить»' "$output" 'add placeholder is visible'
|
||||
assert_contains 'Действие «Удалить»' "$output" 'remove placeholder is visible'
|
||||
assert_equal 0 "$(grep -c '^gum pager$' "$TEST_MOCK_LOG" | awk '$1 >= 3 { print 0; next } { print 1 }')" 'diagnose/log/backups reach pager'
|
||||
printf 'Выход\n' >"$script_file"
|
||||
set +e; main >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal 0 "$status" 'zero-argument main enters TUI'
|
||||
printf 'Выход\n' >"$script_file"
|
||||
set +e; main tui >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal 0 "$status" 'explicit tui command exits cleanly'
|
||||
assert_equal '' "${TUI_GUM_PATH:-}" 'explicit tui clears resolved gum path'
|
||||
;;
|
||||
tui-cancel)
|
||||
prepare_m9_fixture
|
||||
script_file="${TEST_ROOT}/gum-script"; : >"$script_file"; export GUM_SCRIPT_FILE="$script_file"
|
||||
begin_test 'TUI cancellation returns normally without mutation'
|
||||
set +e; tui_screen >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal 0 "$status" 'menu cancellation'
|
||||
assert_equal '' "$(grep -E 'wg (syncconf|quick)|wg-quick' "$TEST_MOCK_LOG" || true)" 'cancellation has no mutation command'
|
||||
printf 'Клиенты\n\nВыход\n' >"$script_file"
|
||||
: >"$TEST_MOCK_LOG"
|
||||
set +e; tui_screen >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal 0 "$status" 'client filter cancellation returns to menu'
|
||||
assert_equal 2 "$(grep '^gum choose' "$TEST_MOCK_LOG" | grep -vc -- '--help' || true)" 'filter cancellation returns to main menu'
|
||||
assert_equal 1 "$(grep '^gum filter' "$TEST_MOCK_LOG" | grep -vc -- '--help' || true)" 'filter cancellation invokes one filter'
|
||||
assert_equal 1 "$(grep '^gum pager' "$TEST_MOCK_LOG" | grep -vc -- '--help' || true)" 'filter cancellation only pages the client list'
|
||||
assert_equal 0 "$(grep -c 'TUI завершён\|Неизвестный пункт' "$TEST_MOCK_LOG" || true)" 'filter cancellation is not a route failure'
|
||||
assert_equal '' "${TUI_GUM_PATH:-}" 'TUI gum path is cleared after cancellation'
|
||||
unset WG_MANAGER_TEST_TTY
|
||||
set +e; tui_is_terminal >/dev/null 2>&1; status=$?; set -e
|
||||
assert_failure "$status" 'unset test TTY is not accepted'
|
||||
;;
|
||||
tui-read-only)
|
||||
prepare_m9_fixture
|
||||
script_file="${TEST_ROOT}/gum-script"; printf 'Клиенты\nalice — BBBBBBBB\nВыход\n' >"$script_file"; export GUM_SCRIPT_FILE="$script_file"
|
||||
snapshot_before=$(find "${TEST_ROOT}/backups" "${TEST_ROOT}/logs" "${TEST_ROOT}/archive" "${TEST_ROOT}/clients" "${TEST_ROOT}/metadata" -mindepth 1 -maxdepth 2 -printf '%P %y %m %s\n' | LC_ALL=C sort; sha256sum "$WG_CONF" "$WG_MANAGER_CONFIG" "${TEST_ROOT}/clients/alice.conf" "${TEST_ROOT}/metadata/alice.meta"; stat -c '%a:%u:%g:%h' "$WG_CONF" "$WG_MANAGER_CONFIG" "${TEST_ROOT}/clients/alice.conf" "${TEST_ROOT}/metadata/alice.meta")
|
||||
begin_test 'TUI client selection uses stable identity and read-only backend'
|
||||
set +e; tui_screen >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal 0 "$status" 'client route exits cleanly'
|
||||
assert_contains 'gum filter' "$(<"$TEST_MOCK_LOG")" 'filter wrapper invocation recorded'
|
||||
pager_count=$(grep -c '^gum pager$' "$TEST_MOCK_LOG" || true)
|
||||
((pager_count >= 1)) && ((TEST_ASSERTIONS += 1)) || record_failure 'client list is paged before selection'
|
||||
assert_equal '' "$(grep -E 'syncconf|wg-quick (up|down|save)|ip (add|del|replace)' "$TEST_MOCK_LOG" || true)" 'read-only screen has no mutation calls'
|
||||
CLIENT_NAMES=(same same) CLIENT_KEYS=(ABCDEFGH111111111111111111111111111111111= ABCDEFGH222222222222222222222222222222222=)
|
||||
CLIENT_ADDRESSES=(192.0.2.2/32 192.0.2.2/32) CLIENT_ENDPOINTS=(- -) CLIENT_HANDSHAKES=(0 0) CLIENT_RX=(0 0) CLIENT_TX=(0 0) CLIENT_STATES=(unmanaged unmanaged) CLIENT_ORIGINS=(unmanaged unmanaged)
|
||||
printf 'same — ABCDEFGH #2\n' >"$script_file"
|
||||
set +e; tui_select_client >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal 0 "$status" 'duplicate display names select the offered stable index'
|
||||
assert_contains 'ABCDEFGH222222222222222222222222222222222=' "$(<"$GUM_PAGER_CAPTURE")" 'selected duplicate identity renders the second full key'
|
||||
assert_equal 0 "$(grep -c 'ABCDEFGH111111111111111111111111111111111=' "$GUM_PAGER_CAPTURE" || true)" 'selected detail does not render the first key'
|
||||
long_name=$(printf 'n%.0s' {1..64})
|
||||
CLIENT_NAMES=("$long_name" "$long_name") CLIENT_KEYS=(ABCDEFGH111111111111111111111111111111111= ABCDEFGH222222222222222222222222222222222=)
|
||||
CLIENT_ADDRESSES=(192.0.2.2/32 192.0.2.2/32)
|
||||
printf '%s\n' "${long_name:0:60} — ABCDEFGH #2" >"$script_file"
|
||||
set +e; tui_select_client >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal 0 "$status" 'long client names remain selectable'
|
||||
assert_equal 0 "$(awk 'length($0) > 80 { bad += 1 } END { print bad + 0 }' "$GUM_CHOICES_CAPTURE")" 'offered client labels fit 80 columns'
|
||||
assert_contains 'ABCDEFGH222222222222222222222222222222222=' "$(<"$GUM_PAGER_CAPTURE")" 'long-name collision keeps the selected identity'
|
||||
assert_equal "$snapshot_before" "$(find "${TEST_ROOT}/backups" "${TEST_ROOT}/logs" "${TEST_ROOT}/archive" "${TEST_ROOT}/clients" "${TEST_ROOT}/metadata" -mindepth 1 -maxdepth 2 -printf '%P %y %m %s\n' | LC_ALL=C sort; sha256sum "$WG_CONF" "$WG_MANAGER_CONFIG" "${TEST_ROOT}/clients/alice.conf" "${TEST_ROOT}/metadata/alice.meta"; stat -c '%a:%u:%g:%h' "$WG_CONF" "$WG_MANAGER_CONFIG" "${TEST_ROOT}/clients/alice.conf" "${TEST_ROOT}/metadata/alice.meta")" 'read-only files, modes, and directory entries unchanged'
|
||||
assert_equal '' "$(grep -E 'syncconf|wg-quick (up|down|save)|ip (add|del|replace)|systemctl (start|stop|restart)|nft (add|delete)|iptables -.*-(A|D|I)' "$TEST_MOCK_LOG" || true)" 'read-only command log has no mutation'
|
||||
assert_equal 0 "$(find "${WG_CONF%/*}" -maxdepth 1 -name '.wg-manager-tui.*' -print | wc -l)" 'read-only pages create no config-directory temporary files'
|
||||
if [[ -e $WG_MANAGER_LOCK_FILE ]]; then lock_status=1; else lock_status=0; fi
|
||||
assert_equal 0 "$lock_status" 'read-only screens do not create a mutation lock'
|
||||
assert_equal 0 "$(grep -hE 'AAAAAAAAAAAAAAAA|PresharedKey' "$GUM_PAGER_CAPTURE" "$GUM_STYLE_CAPTURE" "$TEST_MOCK_LOG" | wc -l)" 'read-only TUI output has no secrets'
|
||||
;;
|
||||
tui-no-gum)
|
||||
prepare_m9_fixture
|
||||
export WG_MANAGER_GUM="${TEST_ROOT}/missing-gum"
|
||||
begin_test 'TUI without gum reports dependency while CLI remains independent'
|
||||
set +e; output=$(tui_screen 2>&1); status=$?; set -e
|
||||
assert_equal "$EXIT_DEPENDENCY" "$status" 'missing gum status'
|
||||
assert_contains 'WG_MANAGER_GUM' "$output" 'missing gum diagnostic'
|
||||
assert_equal '' "${TUI_GUM_PATH:-}" 'gum path is cleared after dependency failure'
|
||||
set +e; output=$(main status 2>&1); status=$?; set -e
|
||||
assert_equal 0 "$status" 'CLI status remains independent of gum'
|
||||
;;
|
||||
tui-non-tty)
|
||||
prepare_m9_fixture
|
||||
unset WG_MANAGER_TEST_TTY
|
||||
begin_test 'TUI refuses noninteractive input before gum output'
|
||||
set +e; output=$(tui_screen 2>&1); status=$?; set -e
|
||||
assert_equal "$EXIT_USAGE" "$status" 'non-TTY status'
|
||||
assert_contains 'интерактивный терминал' "$output" 'non-TTY diagnostic'
|
||||
assert_equal '' "$(<"$TEST_MOCK_LOG")" 'non-TTY has no gum invocation'
|
||||
for tty_mode in stdin stdout none; do
|
||||
export WG_MANAGER_TEST_TTY="$tty_mode"
|
||||
set +e; tui_is_terminal >/dev/null 2>&1; status=$?; set -e
|
||||
assert_failure "$status" "TTY режим ${tty_mode} отклонён"
|
||||
done
|
||||
export WG_MANAGER_TEST_TTY=both
|
||||
set +e; tui_is_terminal >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal 0 "$status" 'оба потока TTY принимаются в изолированном режиме'
|
||||
set +e; main tui extra >/dev/null 2>&1; status=$?; set -e
|
||||
assert_equal "$EXIT_USAGE" "$status" 'tui extra argument is usage'
|
||||
;;
|
||||
esac
|
||||
unset GUM_SCRIPT_FILE WG_MANAGER_GUM WG_MANAGER_TEST_TTY WG_MANAGER_IPV4_FORWARD_FILE WG_MANAGER_IPV6_FORWARD_FILE GUM_STYLE_CAPTURE GUM_PAGER_CAPTURE GUM_CHOICES_CAPTURE GUM_FAIL_CAPABILITY
|
||||
}
|
||||
|
||||
run_dependency_tests() {
|
||||
@@ -3324,6 +3502,8 @@ elif [[ $test_group == add-client || $test_group == address-allocation || $test_
|
||||
run_m4_tests
|
||||
elif [[ $test_group == remove-client || $test_group == remove-cancel || $test_group == remove-exact-block || $test_group == remove-rollback || $test_group == archive-client || $test_group == archive-path-safety || $test_group == remove-preserves-neighbors ]]; then
|
||||
run_m6_tests
|
||||
elif [[ $test_group == gum-resolution || $test_group == tui-routing || $test_group == tui-cancel || $test_group == tui-read-only || $test_group == tui-no-gum || $test_group == tui-non-tty ]]; then
|
||||
run_m9_tests
|
||||
elif [[ $test_group == audit || $test_group == audit-no-secrets || $test_group == backup-list || $test_group == restore || $test_group == restore-invalid || $test_group == restore-rollback || $test_group == backup-path-safety || $test_group == runtime-file-modes ]]; then
|
||||
run_m8_tests
|
||||
elif [[ $test_group == diagnostics || $test_group == diagnostics-systemd || $test_group == diagnostics-firewall || $test_group == diagnostics-forwarding || $test_group == diagnostics-runtime-diff || $test_group == diagnostics-read-only || $test_group == diagnostics-exit-codes ]]; then
|
||||
@@ -3350,6 +3530,11 @@ else
|
||||
run_m8_tests
|
||||
done
|
||||
unset m8_group
|
||||
for m9_group in gum-resolution tui-routing tui-cancel tui-read-only tui-no-gum tui-non-tty; do
|
||||
test_group=$m9_group
|
||||
run_m9_tests
|
||||
done
|
||||
unset m9_group
|
||||
test_group=''
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user