Files
3x-ui/deploy/test/installer-panel-port.sh
T
Hamed 728c2fb94f fix(install): handle yes responses and validate panel ports (#6992)
The panel-port prompt accepted only y/Y, so yes picked a random port; a custom port was never validated (empty, non-numeric, out-of-range, busy, or leading-zero octal), is_port_in_use never detected a listener, and the installer printed success even when applying the settings failed. Answers are now case-insensitive, custom ports are normalized and checked, the port check is fixed, and the install aborts unless the CLI reports every setting applied. A bash suite runs in shell-lint.
2026-11-03 15:52:14 +01:00

305 lines
9.9 KiB
Bash

#!/usr/bin/env bash
# Exercise only installer function definitions, never its installation entry point.
set -euo pipefail
repo_root=$(cd "$(dirname "$0")/../.." && pwd)
installer=${INSTALLER_UNDER_TEST:-"$repo_root/install.sh"}
test_dir=$(mktemp -d)
trap 'rm -rf -- "$test_dir"' EXIT
load_function() {
local definition
definition=$(awk -v name="$1" '
$0 == name "() {" {copy = 1}
copy {print}
copy && /^}$/ {exit}
' "$installer")
[[ -n "$definition" ]] || { echo "Missing installer function: $1" >&2; return 1; }
eval "$definition"
}
load_function is_port_in_use
load_function config_after_install
# The original installer has no selection helpers; its integration tests still run.
if grep -q '^select_panel_port() {' "$installer"; then
load_function normalize_panel_port
load_function select_panel_port
fi
# These commands are invoked by the extracted installer functions in a subshell.
# shellcheck disable=SC2317,SC2329
curl() { printf '192.0.2.1\n200\n'; }
# shellcheck disable=SC2317,SC2329
gen_random_string() { printf 'test-credential-%s\n' "$1"; }
# shellcheck disable=SC2317,SC2329
prompt_and_setup_ssl() {
echo ssl >> "$CASE_DIR/events"
export SSL_SCHEME=http SSL_HOST=192.0.2.1
}
# shellcheck disable=SC2317,SC2329
write_install_result() { printf '%s\n' "$@" > "$CASE_DIR/result"; }
# shellcheck disable=SC2317,SC2329
read() {
echo read >> "$CASE_DIR/events"
[[ "$NONINTERACTIVE" != 1 ]] || return 90
builtin read -r "$@"
}
# shellcheck disable=SC2317,SC2329
shuf() {
echo random >> "$CASE_DIR/events"
local count=0
[[ ! -f "$CASE_DIR/random-count" ]] || count=$(cat "$CASE_DIR/random-count")
echo "$((count + 1))" > "$CASE_DIR/random-count"
[[ "${SHUF_FAIL:-0}" == 0 ]] || return 1
if ((count > 0)) && [[ -n "${RANDOM_NEXT:-}" ]]; then
printf '%s\n' "$RANDOM_NEXT"
else
printf '54321\n'
fi
}
# Select the availability backend deterministically, regardless of host packages.
# shellcheck disable=SC2317,SC2329
command() {
if [[ "${1:-}" == -v && "${2:-}" =~ ^(ss|netstat|lsof)$ ]]; then
[[ "$2" == "${PORT_BACKEND:-ss}" ]]
else
builtin command "$@"
fi
}
# shellcheck disable=SC2317,SC2329
ss() {
printf 'State Recv-Q Send-Q Local\ Address:Port Peer\ Address:Port\n'
if [[ -n "${BUSY_PORT:-}" ]]; then
printf 'LISTEN 0 128 0.0.0.0:%s 0.0.0.0:*\n' "$BUSY_PORT"
printf 'LISTEN 0 128 [::]:%s [::]:*\n' "$BUSY_PORT"
fi
}
# shellcheck disable=SC2317,SC2329
netstat() {
if [[ -n "${BUSY_PORT:-}" ]]; then
printf 'tcp 0 0 0.0.0.0:%s 0.0.0.0:* LISTEN\n' "$BUSY_PORT"
printf 'tcp6 0 0 :::%s :::* LISTEN\n' "$BUSY_PORT"
fi
}
# shellcheck disable=SC2317,SC2329
lsof() { [[ -n "${BUSY_PORT:-}" && "$*" == *"-iTCP:$BUSY_PORT "* ]]; }
new_case() {
CASE_DIR=$(mktemp -d "$test_dir/case.XXXXXXXX")
export CASE_DIR
: > "$CASE_DIR/events"
xui_folder="$CASE_DIR/bin"
mkdir -p "$xui_folder"
cat > "$xui_folder/x-ui" <<'MOCK'
#!/usr/bin/env bash
set -eu
if [[ "${1:-}" == migrate ]]; then
echo migrate >> "$CASE_DIR/events"
exit 0
fi
case "${2:-}" in
-show) printf 'hasDefaultCredential: true\nwebBasePath: /\nport: 2053\n' ;;
-getCert) printf 'cert: \n' ;;
-getApiToken) printf 'apiToken: test-token\n' ;;
-username)
printf '%s\n' "$@" > "$CASE_DIR/settings-args"
case "${SETTING_FAILURE:-}" in
exit) echo 'controlled settings failure' >&2; exit 23 ;;
zero) echo 'Database initialization failed: controlled failure'; exit 0 ;;
esac
while (($#)); do
case "$1" in -port) port=$2 ;; esac
shift
done
printf 'Port set successfully: %s\n' "$port"
[[ "${SETTING_FAILURE:-}" != partial ]] || exit 0
printf 'Username and password updated successfully\nBase URI path set successfully\n'
;;
*) echo 'Unexpected configuration command' >&2; exit 1 ;;
esac
MOCK
chmod +x "$xui_folder/x-ui"
export red='' green='' blue='' yellow='' plain='' release=ubuntu
export XUI_USERNAME=test-user XUI_PASSWORD=test-pass XUI_WEB_BASE_PATH=test-path
unset XUI_PANEL_PORT SETTING_FAILURE BUSY_PORT RANDOM_NEXT SHUF_FAIL
PORT_BACKEND=ss
NONINTERACTIVE=0
}
run_config() {
local input=$1
# The initial newline selects the existing SQLite default. All later input
# belongs to the port prompts; SSL and persistence are side-effect-free mocks.
printf '\n%s' "$input" > "$CASE_DIR/input"
status=0
(config_after_install) < "$CASE_DIR/input" > "$CASE_DIR/output" 2>&1 || status=$?
}
expect_port() {
[[ "$status" == 0 ]] || { cat "$CASE_DIR/output"; return 1; }
local actual
actual=$(awk 'last == "-port" {print; exit} {last = $0}' "$CASE_DIR/settings-args")
[[ "$actual" == "$1" ]] || { echo "Port $actual, expected $1"; return 1; }
grep -Fxq 'Panel Installation Complete!' < <(sed 's/^[[:space:]]*//;s/[[:space:]]*$//' "$CASE_DIR/output")
[[ -f "$CASE_DIR/result" ]] || return 1
[[ $(sed -n '3p' "$CASE_DIR/result") == "$1" ]] || return 1
[[ $(sed -n '1p' "$CASE_DIR/result") == test-user ]] || return 1
[[ $(sed -n '2p' "$CASE_DIR/result") == test-pass ]] || return 1
[[ $(sed -n '4p' "$CASE_DIR/result") == test-path ]] || return 1
}
expect_failure() {
[[ "$status" != 0 ]] || { echo 'Configuration unexpectedly succeeded'; return 1; }
[[ ! -f "$CASE_DIR/result" ]] || return 1
! grep -q 'Panel Installation Complete!\|SSL Certificate Setup\|^ssl$' "$CASE_DIR/output" "$CASE_DIR/events"
}
affirmative() {
new_case
run_config "$1"$'\n2053\n'
expect_port 2053
! grep -q '^random$' "$CASE_DIR/events"
}
negative() {
new_case
run_config "$1"$'\n'
expect_port 54321
[[ $(grep -c '^random$' "$CASE_DIR/events") == 1 ]]
}
invalid_port() {
new_case
run_config $'y\n'"$1"$'\n2053\n'
expect_port 2053
grep -q 'Invalid port' "$CASE_DIR/output"
! grep -q '^random$' "$CASE_DIR/events"
}
valid_port() {
new_case
run_config $'y\n'"$1"$'\n'
expect_port "$2"
}
confirmation_retry() {
new_case
run_config $'maybe\nyes please\nYES\n2053\n'
expect_port 2053
grep -q 'Please answer yes or no' "$CASE_DIR/output"
! grep -q '^random$' "$CASE_DIR/events"
}
busy_port() {
new_case
BUSY_PORT=2053
PORT_BACKEND=$1
run_config $'y\n2053\n2054\n'
expect_port 2054
grep -q 'already in use' "$CASE_DIR/output"
}
random_retry() {
new_case
BUSY_PORT=54321 RANDOM_NEXT=54322
run_config $'no\n'
expect_port 54322
[[ $(grep -c '^random$' "$CASE_DIR/events") == 2 ]]
}
random_failure() {
new_case
SHUF_FAIL=1
run_config $'no\n'
expect_failure
[[ ! -f "$CASE_DIR/settings-args" ]]
}
random_exhaustion() {
new_case
BUSY_PORT=54321
run_config $'no\n'
expect_failure
[[ ! -f "$CASE_DIR/settings-args" ]]
[[ $(grep -c '^random$' "$CASE_DIR/events") == 100 ]]
}
noninteractive() {
new_case
NONINTERACTIVE=1
case "$1" in unset) ;; *) export XUI_PANEL_PORT=$1 ;; esac
run_config ''
expect_port "$2"
! grep -q '^read$' "$CASE_DIR/events"
}
noninteractive_invalid() {
new_case
NONINTERACTIVE=1
export XUI_PANEL_PORT=$1
run_config ''
expect_failure
[[ ! -f "$CASE_DIR/settings-args" ]]
! grep -q '^read$\|^random$' "$CASE_DIR/events"
}
noninteractive_busy() {
new_case
NONINTERACTIVE=1 BUSY_PORT=2053
export XUI_PANEL_PORT=2053
run_config ''
expect_failure
[[ ! -f "$CASE_DIR/settings-args" ]]
! grep -q '^read$\|^random$' "$CASE_DIR/events"
}
settings_failure() {
new_case
export SETTING_FAILURE=$1
run_config $'y\n2053\n'
expect_failure
[[ -f "$CASE_DIR/settings-args" ]]
grep -q 'Failed to configure panel settings' "$CASE_DIR/output"
}
input_eof() {
new_case
run_config "$1"
expect_failure
[[ ! -f "$CASE_DIR/settings-args" ]]
}
passed=0 failed=0
check() {
local name=$1
shift
# Keep the test body out of an if/|| condition: those disable errexit
# throughout called functions and can hide a failed intermediate assertion.
set +e
(set -e; "$@")
local result=$?
set -e
if [[ "$result" == 0 ]]; then
printf 'PASS %s\n' "$name"
passed=$((passed + 1))
else
printf 'FAIL %s\n' "$name" >&2
failed=$((failed + 1))
fi
}
for answer in y Y yes Yes YES ' yEs ' $'\tYES\t'; do check "affirmative [$answer]" affirmative "$answer"; done
for answer in n N no No NO '' ' nO ' $' \t \r '; do check "negative [$answer]" negative "$answer"; done
check 'invalid confirmation retries' confirmation_retry
for port in '' abc 0 -1 65536 00065536 70000 1.5 +2053 '20 53' 0x805 999999999999999999999999 00000 '1+2' 'a[0]'; do
check "invalid port [$port]" invalid_port "$port"
done
for port in 1 2053 65535; do check "valid port [$port]" valid_port "$port" "$port"; done
check 'leading zeros use decimal' valid_port 00002053 2053
check 'leading zeros containing eight use decimal' valid_port 008080 8080
check 'leading-zero boundary uses decimal' valid_port 065535 65535
for backend in ss netstat lsof; do check "busy port via $backend" busy_port "$backend"; done
check 'busy random port retried' random_retry
check 'random generator failure' random_failure
check 'random search exhaustion' random_exhaustion
check 'noninteractive supplied port' noninteractive 2053 2053
check 'noninteractive decimal normalization' noninteractive 00002053 2053
check 'noninteractive random default' noninteractive unset 54321
for port in '' abc 0 -1 65536 1.5 999999999999999999999; do
check "noninteractive invalid [$port]" noninteractive_invalid "$port"
done
check 'noninteractive busy port fails' noninteractive_busy
for failure in exit zero partial; do check "settings failure [$failure]" settings_failure "$failure"; done
check 'EOF at confirmation' input_eof ''
check 'EOF at custom port' input_eof $'y\n'
printf '%s passed, %s failed\n' "$passed" "$failed"
[[ "$failed" == 0 ]]