From f679515a8fe22d8e6c254487acd2b63d9dbc7190 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Tue, 21 Jul 2026 22:22:04 +0300 Subject: [PATCH] =?UTF-8?q?feat(proxy-client):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=81=D0=B0=D0=BC=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D1=8F=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D1=89=D0=B8=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - пакет должен устанавливаться отдельно от dotfiles без ручной раскладки и опасных замен. - Что: - добавлен безопасный установщик со Stow и прямыми симлинками. - добавлены идемпотентная настройка Bash и защита машинных файлов. - обновлены инструкция и полный набор интеграционных проверок. - Проверка: - env XDG_STATE_HOME=<временный каталог> bash tests/test_proxy_client_full.sh. --- proxy-client/.stow-local-ignore | 3 + proxy-client/README.md | 35 ++- proxy-client/install.sh | 216 ++++++++++++++++ tests/test_proxy_client_full.sh | 1 + tests/test_proxy_client_install.sh | 402 +++++++++++++++++++++++++++++ 5 files changed, 645 insertions(+), 12 deletions(-) create mode 100755 proxy-client/install.sh create mode 100755 tests/test_proxy_client_install.sh diff --git a/proxy-client/.stow-local-ignore b/proxy-client/.stow-local-ignore index 8d287b5..9d84037 100644 --- a/proxy-client/.stow-local-ignore +++ b/proxy-client/.stow-local-ignore @@ -1 +1,4 @@ ^/README\.md$ +^/install\.sh$ +^/\.config/proxy-client/config$ +^/\.config/proxy-client/default\.url$ diff --git a/proxy-client/README.md b/proxy-client/README.md index 6c26514..50933cd 100644 --- a/proxy-client/README.md +++ b/proxy-client/README.md @@ -32,20 +32,31 @@ source ~/.bash_custom ### Только пакет `proxy-client` -```bash -cd ~/dotfiles -mkdir -p ~/.local/bin ~/.config/proxy-client -stow proxy-client -source ~/.config/proxy-client/shell.bash -``` - -Если Bash-пакет dotfiles не установлен, добавьте подключение модуля в -`~/.bashrc`: +Каталог можно скопировать отдельно от остальных dotfiles и запустить его +собственный установщик из любого рабочего каталога: ```bash -printf '%s\n' 'source ~/.config/proxy-client/shell.bash' >> ~/.bashrc +~/src/proxy-client/install.sh ``` +При наличии Stow установщик использует его, иначе создаёт те же три симлинка +напрямую: CLI, Bash-модуль и каталог шаблонов. Исходный каталог после установки +нельзя перемещать: домашние ссылки зависят от его постоянного пути. + +По умолчанию установщик добавляет в `~/.bashrc` помеченный блок для +`~/.local/bin` и Bash-модуля. Чтобы оставить настройки оболочки без изменений: + +```bash +~/src/proxy-client/install.sh --no-shell-setup +``` + +Установщик не создаёт машинный `config` и закрытый `default.url`, не +устанавливает Xray и не проверяет сеть. После установки примените изменения +через `source ~/.bashrc`; с `--no-shell-setup` подключите модуль напрямую: +`source ~/.config/proxy-client/shell.bash`. Затем выберите режим по разделу +[«Настройка режима»](#настройка-режима). Без машинного конфига продолжает +действовать режим Hiddify по умолчанию. + ## Как выбирается прокси Режим определяется только машинным файлом @@ -247,9 +258,9 @@ export PROXY_PREFLIGHT_TIMEOUT=5 ## Справочник -### Ручное подключение без Stow +### Ручное подключение без установщика -Можно создать ссылки на CLI, Bash-модуль и примеры либо скопировать те же файлы: +Если пакетный установщик запустить нельзя, создайте те же ссылки вручную: ```bash mkdir -p ~/.local/bin ~/.config/proxy-client diff --git a/proxy-client/install.sh b/proxy-client/install.sh new file mode 100755 index 0000000..e36dda6 --- /dev/null +++ b/proxy-client/install.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash +set -euo pipefail + +PACKAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +STOW_BIN="${STOW_BIN:-stow}" +SETUP_SHELL=1 + +usage() { + cat <<'EOF' +Использование: ./install.sh [--no-shell-setup] + + --no-shell-setup не изменять ~/.bashrc +EOF +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --no-shell-setup) + SETUP_SHELL=0 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Ошибка: неизвестный параметр: $1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +SOURCES=( + "$PACKAGE_DIR/.local/bin/proxy-client" + "$PACKAGE_DIR/.config/proxy-client/shell.bash" + "$PACKAGE_DIR/.config/proxy-client/examples" +) +TARGETS=( + "$HOME/.local/bin/proxy-client" + "$HOME/.config/proxy-client/shell.bash" + "$HOME/.config/proxy-client/examples" +) +STOW_IGNORE_PATTERNS=( + '^\.local/bin/proxy-client$' + '^\.config/proxy-client/shell\.bash$' + '^\.config/proxy-client/examples$' +) + +source_errors=() +[ -x "${SOURCES[0]}" ] || source_errors+=("${SOURCES[0]}") +[ -f "${SOURCES[1]}" ] || source_errors+=("${SOURCES[1]}") +[ -d "${SOURCES[2]}" ] || source_errors+=("${SOURCES[2]}") +if [ "${#source_errors[@]}" -gt 0 ]; then + echo "Ошибка: отсутствуют обязательные файлы исходного пакета:" >&2 + printf ' %s\n' "${source_errors[@]}" >&2 + exit 1 +fi + +"${SOURCES[0]}" --help >/dev/null +bash -n "${SOURCES[1]}" + +SHELL_BLOCK_START='# >>> proxy-client >>>' +SHELL_BLOCK_END='# <<< proxy-client <<<' +SHELL_SETUP_NEEDED=0 +BASH_CUSTOM_HAS_MODULE=0 +SHELL_PATH_NEEDED=1 +SHELL_SOURCE_NEEDED=1 +SHELL_BLOCK_CONFLICT=0 +if [ "$SETUP_SHELL" -eq 1 ]; then + if [ -f "$HOME/.bash_custom" ] \ + && grep -Fq 'proxy-client/shell.bash' "$HOME/.bash_custom"; then + BASH_CUSTOM_HAS_MODULE=1 + SHELL_SOURCE_NEEDED=0 + fi + + bashrc_start_count=0 + bashrc_end_count=0 + if [ -f "$HOME/.bashrc" ]; then + bashrc_start_count="$( + grep -Fxc "$SHELL_BLOCK_START" "$HOME/.bashrc" || true + )" + bashrc_end_count="$( + grep -Fxc "$SHELL_BLOCK_END" "$HOME/.bashrc" || true + )" + if grep -Fq 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc"; then + SHELL_PATH_NEEDED=0 + fi + fi + + if [ "$bashrc_start_count" -eq 0 ] && [ "$bashrc_end_count" -eq 0 ]; then + if [ "$SHELL_PATH_NEEDED" -eq 1 ] \ + || [ "$SHELL_SOURCE_NEEDED" -eq 1 ]; then + SHELL_SETUP_NEEDED=1 + fi + elif [ "$bashrc_start_count" -eq 1 ] \ + && [ "$bashrc_end_count" -eq 1 ] \ + && [ "$SHELL_PATH_NEEDED" -eq 0 ] \ + && { [ "$BASH_CUSTOM_HAS_MODULE" -eq 1 ] \ + || grep -Fq 'proxy-client/shell.bash' "$HOME/.bashrc"; }; then + : + else + SHELL_BLOCK_CONFLICT=1 + fi +fi + +conflicts=() +missing_targets=0 +stow_ignore_args=() +PARENT_DIRS=( + "$HOME/.local" + "$HOME/.local/bin" + "$HOME/.config" + "$HOME/.config/proxy-client" +) +for parent_dir in "${PARENT_DIRS[@]}"; do + if { [ -e "$parent_dir" ] || [ -L "$parent_dir" ]; } \ + && { [ ! -d "$parent_dir" ] || [ -L "$parent_dir" ]; }; then + conflicts+=("$parent_dir") + fi +done + +for index in "${!TARGETS[@]}"; do + target="${TARGETS[$index]}" + source_path="${SOURCES[$index]}" + if [ -e "$target" ] || [ -L "$target" ]; then + if [ ! -L "$target" ] \ + || [ "$(readlink -f "$target" 2>/dev/null || true)" != "$source_path" ]; then + conflicts+=("$target") + else + stow_ignore_args+=("--ignore=${STOW_IGNORE_PATTERNS[$index]}") + fi + else + missing_targets=$((missing_targets + 1)) + fi +done + +if [ "$SHELL_SETUP_NEEDED" -eq 1 ] \ + && { [ -e "$HOME/.bashrc" ] || [ -L "$HOME/.bashrc" ]; } \ + && [ ! -f "$HOME/.bashrc" ]; then + conflicts+=("$HOME/.bashrc") +fi +if [ "$SHELL_BLOCK_CONFLICT" -eq 1 ]; then + conflicts+=("$HOME/.bashrc") +fi + +if [ "${#conflicts[@]}" -gt 0 ]; then + echo "Ошибка: найдены конфликтующие пути:" >&2 + printf ' %s\n' "${conflicts[@]}" >&2 + echo "Разберите их вручную и повторите установку." >&2 + exit 1 +fi + +mkdir -p "$HOME/.local/bin" "$HOME/.config/proxy-client" + +if command -v "$STOW_BIN" >/dev/null 2>&1; then + echo "Установка с помощью Stow." + if [ "$missing_targets" -gt 0 ]; then + "$STOW_BIN" --dir="$(dirname "$PACKAGE_DIR")" --target="$HOME" \ + "${stow_ignore_args[@]}" \ + "$(basename "$PACKAGE_DIR")" + fi +else + echo "Установка без Stow: создаю символические ссылки напрямую." + for index in "${!TARGETS[@]}"; do + target="${TARGETS[$index]}" + source_path="${SOURCES[$index]}" + if [ ! -L "$target" ]; then + ln -s "$source_path" "$target" + fi + done +fi + +"$HOME/.local/bin/proxy-client" --help >/dev/null +bash -n "$HOME/.config/proxy-client/shell.bash" + +if [ "$SHELL_SETUP_NEEDED" -eq 1 ]; then + cat >> "$HOME/.bashrc" <<'EOF' + +# >>> proxy-client >>> +EOF + if [ "$SHELL_PATH_NEEDED" -eq 1 ]; then + cat >> "$HOME/.bashrc" <<'EOF' +case ":$PATH:" in + *":$HOME/.local/bin:"*) ;; + *) + export PATH="$HOME/.local/bin:$PATH" + ;; +esac +EOF + fi + if [ "$SHELL_SOURCE_NEEDED" -eq 1 ]; then + cat >> "$HOME/.bashrc" <<'EOF' +[ -f "$HOME/.config/proxy-client/shell.bash" ] && source "$HOME/.config/proxy-client/shell.bash" +EOF + fi + cat >> "$HOME/.bashrc" <<'EOF' +# <<< proxy-client <<< +EOF +elif [ "$SETUP_SHELL" -eq 0 ]; then + echo "Настройка Bash пропущена (--no-shell-setup)." +fi + +if [ "$SETUP_SHELL" -eq 1 ]; then + RELOAD_COMMAND='source ~/.bashrc' +else + RELOAD_COMMAND='source ~/.config/proxy-client/shell.bash' +fi + +cat <&2 + exit 1 +} + +assert_link_points_to() { + local link_path="$1" expected_path="$2" actual_path + [ -L "$link_path" ] || fail "Expected symlink: $link_path" + actual_path="$(readlink -f "$link_path")" + [ "$actual_path" = "$expected_path" ] \ + || fail "$link_path points to $actual_path, expected $expected_path" +} + +assert_not_exists() { + local path="$1" + [ ! -e "$path" ] && [ ! -L "$path" ] || fail "Path should not exist: $path" +} + +setup_package() { + local fixture_root="$1" package_dir="$fixture_root/source with spaces/proxy-client" + mkdir -p \ + "$package_dir/.local/bin" \ + "$package_dir/.config/proxy-client" + cp -a \ + "$REPO_ROOT/proxy-client/.local/bin/proxy-client" \ + "$package_dir/.local/bin/proxy-client" + cp -a \ + "$REPO_ROOT/proxy-client/.config/proxy-client/shell.bash" \ + "$REPO_ROOT/proxy-client/.config/proxy-client/examples" \ + "$package_dir/.config/proxy-client/" + cp -a \ + "$REPO_ROOT/proxy-client/.stow-local-ignore" \ + "$REPO_ROOT/proxy-client/README.md" \ + "$REPO_ROOT/proxy-client/install.sh" \ + "$package_dir/" + printf '%s\n' 'MACHINE_CONFIG_SENTINEL' > "$package_dir/.config/proxy-client/config" + printf '%s\n' 'PRIVATE_PROFILE_SENTINEL' > "$package_dir/.config/proxy-client/default.url" + printf '%s\n' "$package_dir" +} + +run_package_install() { + local package_dir="$1" home_dir="$2" stow_bin="$3" + shift 3 + ( + cd "$TEST_TMP_ROOT" + HOME="$home_dir" STOW_BIN="$stow_bin" \ + "$package_dir/install.sh" "$@" + ) +} + +test_installs_same_public_artifacts_without_stow() { + local fixture_root package_dir home_dir output + fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")" + package_dir="$(setup_package "$fixture_root")" + home_dir="$fixture_root/home" + mkdir -p "$home_dir" + + output="$( + run_package_install "$package_dir" "$home_dir" missing-stow --no-shell-setup + )" + + printf '%s' "$output" | grep -Fq 'без Stow' \ + || fail "Expected direct-symlink method in output" + printf '%s' "$output" | grep -Fq \ + 'source ~/.config/proxy-client/shell.bash' \ + || fail "Expected direct shell-module hint with --no-shell-setup" + [ -d "$home_dir/.local/bin" ] && [ ! -L "$home_dir/.local/bin" ] \ + || fail "Expected ~/.local/bin to be a real directory" + [ -d "$home_dir/.config/proxy-client" ] \ + && [ ! -L "$home_dir/.config/proxy-client" ] \ + || fail "Expected proxy-client config home to be a real directory" + assert_link_points_to \ + "$home_dir/.local/bin/proxy-client" \ + "$package_dir/.local/bin/proxy-client" + assert_link_points_to \ + "$home_dir/.config/proxy-client/shell.bash" \ + "$package_dir/.config/proxy-client/shell.bash" + assert_link_points_to \ + "$home_dir/.config/proxy-client/examples" \ + "$package_dir/.config/proxy-client/examples" + assert_not_exists "$home_dir/.config/proxy-client/config" + assert_not_exists "$home_dir/.config/proxy-client/default.url" + assert_not_exists "$home_dir/README.md" + assert_not_exists "$home_dir/install.sh" + assert_not_exists "$home_dir/.bashrc" +} + +test_configures_bash_once_by_default() { + local fixture_root package_dir home_dir output marker_count path_count source_count + fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")" + package_dir="$(setup_package "$fixture_root")" + home_dir="$fixture_root/home" + mkdir -p "$home_dir" + + run_package_install "$package_dir" "$home_dir" missing-stow >/dev/null + output="$(run_package_install "$package_dir" "$home_dir" missing-stow)" + + marker_count="$(grep -Fxc '# >>> proxy-client >>>' "$home_dir/.bashrc")" + path_count="$(grep -Fc 'export PATH="$HOME/.local/bin:$PATH"' "$home_dir/.bashrc")" + source_count="$( + grep -Fc \ + '[ -f "$HOME/.config/proxy-client/shell.bash" ] && source "$HOME/.config/proxy-client/shell.bash"' \ + "$home_dir/.bashrc" + )" + [ "$marker_count" -eq 1 ] || fail "Expected one managed Bash block" + [ "$path_count" -eq 1 ] || fail "Expected one ~/.local/bin PATH line" + [ "$source_count" -eq 1 ] || fail "Expected one shell module source line" + printf '%s' "$output" | grep -Fq 'source ~/.bashrc' \ + || fail "Expected current-session reload hint" +} + +test_installs_same_public_artifacts_with_stow() { + local fixture_root package_dir home_dir output + fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")" + package_dir="$(setup_package "$fixture_root")" + home_dir="$fixture_root/home" + mkdir -p "$home_dir" + + output="$(run_package_install "$package_dir" "$home_dir" stow --no-shell-setup)" + + printf '%s' "$output" | grep -Fq 'с помощью Stow' \ + || fail "Expected Stow method in output" + [ -d "$home_dir/.local/bin" ] && [ ! -L "$home_dir/.local/bin" ] \ + || fail "Expected ~/.local/bin to be a real directory" + [ -d "$home_dir/.config/proxy-client" ] \ + && [ ! -L "$home_dir/.config/proxy-client" ] \ + || fail "Expected proxy-client config home to be a real directory" + assert_link_points_to \ + "$home_dir/.local/bin/proxy-client" \ + "$package_dir/.local/bin/proxy-client" + assert_link_points_to \ + "$home_dir/.config/proxy-client/shell.bash" \ + "$package_dir/.config/proxy-client/shell.bash" + assert_link_points_to \ + "$home_dir/.config/proxy-client/examples" \ + "$package_dir/.config/proxy-client/examples" + assert_not_exists "$home_dir/.config/proxy-client/config" + assert_not_exists "$home_dir/.config/proxy-client/default.url" + assert_not_exists "$home_dir/README.md" + assert_not_exists "$home_dir/install.sh" +} + +test_rejects_linked_parent_directories_before_changes() { + local fixture_root package_dir home_dir external_local external_config output + fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")" + package_dir="$(setup_package "$fixture_root")" + home_dir="$fixture_root/home" + external_local="$fixture_root/external-local" + external_config="$fixture_root/external-config" + mkdir -p "$home_dir" "$external_local" "$external_config" + ln -s "$external_local" "$home_dir/.local" + ln -s "$external_config" "$home_dir/.config" + + if output="$( + run_package_install "$package_dir" "$home_dir" missing-stow 2>&1 + )"; then + fail "Expected linked parent directories to be rejected" + fi + + printf '%s' "$output" | grep -Fq "$home_dir/.local" \ + || fail "Expected ~/.local conflict in output" + printf '%s' "$output" | grep -Fq "$home_dir/.config" \ + || fail "Expected ~/.config conflict in output" + assert_not_exists "$external_local/bin" + assert_not_exists "$external_config/proxy-client" + assert_not_exists "$home_dir/.bashrc" +} + +test_reports_all_target_conflicts_without_partial_install() { + local fixture_root package_dir home_dir other_target output + fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")" + package_dir="$(setup_package "$fixture_root")" + home_dir="$fixture_root/home" + other_target="$fixture_root/other-target" + mkdir -p "$home_dir/.local/bin" "$home_dir/.config/proxy-client/shell.bash" + printf '%s\n' 'KEEP_CLI' > "$home_dir/.local/bin/proxy-client" + printf '%s\n' 'KEEP_TARGET' > "$other_target" + ln -s "$other_target" "$home_dir/.config/proxy-client/examples" + + if output="$( + run_package_install "$package_dir" "$home_dir" missing-stow 2>&1 + )"; then + fail "Expected all target conflicts to be rejected" + fi + + printf '%s' "$output" | grep -Fq "$home_dir/.local/bin/proxy-client" \ + || fail "Expected CLI conflict in output" + printf '%s' "$output" | grep -Fq "$home_dir/.config/proxy-client/shell.bash" \ + || fail "Expected shell module conflict in output" + printf '%s' "$output" | grep -Fq "$home_dir/.config/proxy-client/examples" \ + || fail "Expected examples conflict in output" + grep -Fq 'KEEP_CLI' "$home_dir/.local/bin/proxy-client" \ + || fail "Expected conflicting CLI file to stay unchanged" + [ -d "$home_dir/.config/proxy-client/shell.bash" ] \ + || fail "Expected conflicting shell directory to stay unchanged" + assert_link_points_to "$home_dir/.config/proxy-client/examples" "$other_target" + assert_not_exists "$home_dir/.bashrc" +} + +test_rejects_broken_link_without_creating_other_targets() { + local fixture_root package_dir home_dir output + fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")" + package_dir="$(setup_package "$fixture_root")" + home_dir="$fixture_root/home" + mkdir -p "$home_dir/.local/bin" + ln -s "$fixture_root/missing-cli" "$home_dir/.local/bin/proxy-client" + + if output="$( + run_package_install "$package_dir" "$home_dir" missing-stow 2>&1 + )"; then + fail "Expected broken target link to be rejected" + fi + + printf '%s' "$output" | grep -Fq "$home_dir/.local/bin/proxy-client" \ + || fail "Expected broken CLI link in output" + [ -L "$home_dir/.local/bin/proxy-client" ] \ + || fail "Expected broken CLI link to stay unchanged" + assert_not_exists "$home_dir/.config" + assert_not_exists "$home_dir/.bashrc" +} + +test_preserves_machine_files_with_both_methods() { + local stow_bin fixture_root package_dir home_dir + for stow_bin in missing-stow stow; do + fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")" + package_dir="$(setup_package "$fixture_root")" + home_dir="$fixture_root/home" + mkdir -p "$home_dir/.config/proxy-client" + printf '%s\n' 'HOME_CONFIG_SENTINEL' \ + > "$home_dir/.config/proxy-client/config" + printf '%s\n' 'HOME_PROFILE_SENTINEL' \ + > "$home_dir/.config/proxy-client/default.url" + + run_package_install "$package_dir" "$home_dir" "$stow_bin" \ + --no-shell-setup >/dev/null + + [ ! -L "$home_dir/.config/proxy-client/config" ] \ + || fail "Machine config must not become a symlink" + [ ! -L "$home_dir/.config/proxy-client/default.url" ] \ + || fail "Private profile must not become a symlink" + grep -Fq 'HOME_CONFIG_SENTINEL' "$home_dir/.config/proxy-client/config" \ + || fail "Machine config changed" + grep -Fq 'HOME_PROFILE_SENTINEL' "$home_dir/.config/proxy-client/default.url" \ + || fail "Private profile changed" + done +} + +test_accepts_existing_links_when_method_changes() { + local fixture_root package_dir home_dir + fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")" + package_dir="$(setup_package "$fixture_root")" + home_dir="$fixture_root/home" + mkdir -p "$home_dir" + + run_package_install "$package_dir" "$home_dir" missing-stow \ + --no-shell-setup >/dev/null + run_package_install "$package_dir" "$home_dir" stow \ + --no-shell-setup >/dev/null + + assert_link_points_to \ + "$home_dir/.local/bin/proxy-client" \ + "$package_dir/.local/bin/proxy-client" + assert_link_points_to \ + "$home_dir/.config/proxy-client/shell.bash" \ + "$package_dir/.config/proxy-client/shell.bash" + assert_link_points_to \ + "$home_dir/.config/proxy-client/examples" \ + "$package_dir/.config/proxy-client/examples" +} + +test_stow_completes_partial_correct_installation() { + local fixture_root package_dir home_dir + fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")" + package_dir="$(setup_package "$fixture_root")" + home_dir="$fixture_root/home" + mkdir -p "$home_dir" + + run_package_install "$package_dir" "$home_dir" missing-stow \ + --no-shell-setup >/dev/null + rm "$home_dir/.config/proxy-client/shell.bash" + + run_package_install "$package_dir" "$home_dir" stow \ + --no-shell-setup >/dev/null + + assert_link_points_to \ + "$home_dir/.local/bin/proxy-client" \ + "$package_dir/.local/bin/proxy-client" + assert_link_points_to \ + "$home_dir/.config/proxy-client/shell.bash" \ + "$package_dir/.config/proxy-client/shell.bash" + assert_link_points_to \ + "$home_dir/.config/proxy-client/examples" \ + "$package_dir/.config/proxy-client/examples" +} + +test_does_not_duplicate_full_dotfiles_shell_setup() { + local fixture_root package_dir home_dir source_count + fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")" + package_dir="$(setup_package "$fixture_root")" + home_dir="$fixture_root/home" + mkdir -p "$home_dir" + printf '%s\n' \ + '[ -f ~/.config/proxy-client/shell.bash ] && source ~/.config/proxy-client/shell.bash' \ + > "$home_dir/.bash_custom" + printf '%s\n' '[ -f ~/.bash_custom ] && source ~/.bash_custom' \ + > "$home_dir/.bashrc" + + run_package_install "$package_dir" "$home_dir" missing-stow >/dev/null + + grep -Fq '# >>> proxy-client >>>' "$home_dir/.bashrc" \ + || fail "Expected managed PATH block with full dotfiles" + grep -Fq 'export PATH="$HOME/.local/bin:$PATH"' "$home_dir/.bashrc" \ + || fail "Expected ~/.local/bin setup with full dotfiles" + source_count="$(grep -Fc 'proxy-client/shell.bash' "$home_dir/.bashrc" || true)" + [ "$source_count" -eq 0 ] \ + || fail "Standalone block duplicated full dotfiles module source" +} + +test_rejects_damaged_managed_bash_block_before_changes() { + local fixture_root package_dir home_dir output + fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")" + package_dir="$(setup_package "$fixture_root")" + home_dir="$fixture_root/home" + mkdir -p "$home_dir" + printf '%s\n' '# >>> proxy-client >>>' > "$home_dir/.bashrc" + + if output="$( + run_package_install "$package_dir" "$home_dir" missing-stow 2>&1 + )"; then + fail "Expected damaged managed Bash block to be rejected" + fi + + printf '%s' "$output" | grep -Fq "$home_dir/.bashrc" \ + || fail "Expected ~/.bashrc conflict in output" + [ "$(wc -l < "$home_dir/.bashrc")" -eq 1 ] \ + || fail "Expected damaged Bash block to stay unchanged" + assert_not_exists "$home_dir/.local" + assert_not_exists "$home_dir/.config" +} + +test_does_not_duplicate_existing_local_bin_path() { + local fixture_root package_dir home_dir path_count source_count + fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")" + package_dir="$(setup_package "$fixture_root")" + home_dir="$fixture_root/home" + mkdir -p "$home_dir" + printf '%s\n' 'export PATH="$HOME/.local/bin:$PATH"' > "$home_dir/.bashrc" + + run_package_install "$package_dir" "$home_dir" missing-stow >/dev/null + + path_count="$(grep -Fc 'export PATH="$HOME/.local/bin:$PATH"' "$home_dir/.bashrc")" + source_count="$(grep -Fc 'proxy-client/shell.bash' "$home_dir/.bashrc")" + [ "$path_count" -eq 1 ] || fail "Existing ~/.local/bin PATH line was duplicated" + [ "$source_count" -eq 1 ] || fail "Expected shell module source in managed block" +} + +test_rejects_incomplete_source_before_changes() { + local fixture_root package_dir home_dir output + fixture_root="$(mktemp -d "$TEST_TMP_ROOT/fixture.XXXXXX")" + package_dir="$(setup_package "$fixture_root")" + home_dir="$fixture_root/home" + mkdir -p "$home_dir" + rm "$package_dir/.config/proxy-client/shell.bash" + + if output="$( + run_package_install "$package_dir" "$home_dir" missing-stow 2>&1 + )"; then + fail "Expected incomplete source package to be rejected" + fi + + printf '%s' "$output" | grep -Fq 'исходного пакета' \ + || fail "Expected source package error" + assert_not_exists "$home_dir/.local" + assert_not_exists "$home_dir/.config" + assert_not_exists "$home_dir/.bashrc" +} + +main() { + test_installs_same_public_artifacts_without_stow + test_configures_bash_once_by_default + test_installs_same_public_artifacts_with_stow + test_rejects_linked_parent_directories_before_changes + test_reports_all_target_conflicts_without_partial_install + test_rejects_broken_link_without_creating_other_targets + test_preserves_machine_files_with_both_methods + test_accepts_existing_links_when_method_changes + test_stow_completes_partial_correct_installation + test_does_not_duplicate_full_dotfiles_shell_setup + test_rejects_damaged_managed_bash_block_before_changes + test_does_not_duplicate_existing_local_bin_path + test_rejects_incomplete_source_before_changes + echo "All standalone proxy-client install tests passed" +} + +main "$@"