Files
3x-ui/scripts_base_path_test.go
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

288 lines
10 KiB
Go

package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
)
func scriptFunction(t *testing.T, script, name string) string {
t.Helper()
source, err := os.ReadFile(script)
if err != nil {
t.Fatal(err)
}
_, body, ok := strings.Cut("\n"+string(source), "\n"+name+"() {\n")
if !ok {
t.Fatalf("%s: missing function %s", script, name)
}
body, _, ok = strings.Cut(body, "\n}\n")
if !ok {
t.Fatalf("%s: missing closing brace for %s", script, name)
}
return name + "() {\n" + body + "\n}\n"
}
const settingsCLIMock = `#!/bin/bash
set -eu
state=$TEST_SETTINGS_DIR
if [[ "$1" == migrate ]]; then exit 0; fi
shift
case "$1" in
-show)
case "$TEST_READ_MODE" in
failure) exit 1 ;;
empty) exit 0 ;;
diagnostic) echo 'get webBasePath failed, error info: database unavailable' ;;
esac
echo 'current panel settings as follows:'
echo "hasDefaultCredential: $(cat "$state/default")"
echo "port: $(cat "$state/port")"
case "$TEST_READ_MODE" in
missing_field) ;;
malformed) echo 'webBasePath: not-an-absolute-path' ;;
duplicate) printf 'webBasePath: /\nwebBasePath: /a/\n' ;;
*)
path=$(cat "$state/path")
printf 'webBasePath: %s\n' "${path:-/}" ;;
esac
exit 0 ;;
-getCert) echo 'cert: /tmp/panel.example/fullchain.pem'; exit 0 ;;
-getApiToken) echo 'apiToken: test-token'; exit 0 ;;
esac
printf 'write\n' >> "$state/writes"
said_port= said_user= said_path=
while [[ $# -gt 0 ]]; do
key=$1 value=$2
shift 2
case "$key" in
-webBasePath)
said_path=1
printf '%s\n' "$value" >> "$state/path-writes"
[[ "$value" == /* ]] || value="/$value"
[[ "$value" == */ ]] || value="$value/"
printf '%s' "$value" > "$state/path" ;;
-username) said_user=1; printf '%s' "$value" > "$state/username" ;;
-password) said_user=1; printf '%s' "$value" > "$state/password" ;;
-port) said_port=$value; printf '%s' "$value" > "$state/port" ;;
*) echo "unexpected setting: $key" >&2; exit 98 ;;
esac
done
if [[ "$(cat "$state/username")" != admin && "$(cat "$state/password")" != admin ]]; then
echo false > "$state/default"
fi
[[ -z "$said_port" ]] || echo "Port set successfully: $said_port"
[[ -z "$said_user" ]] || echo 'Username and password updated successfully'
[[ -z "$said_path" ]] || echo 'Base URI path set successfully'
`
func runScriptConfig(t *testing.T, script, path string, defaults bool, mode string, runs int, continueSetup bool) (string, string, error) {
t.Helper()
if runtime.GOOS == "windows" {
t.Skip("installer regressions require a POSIX environment; run them under WSL")
}
bash, err := exec.LookPath("bash")
if err != nil {
t.Skip("bash is required for the isolated installer regression tests")
}
dir := t.TempDir()
username, password := "custom-user", "custom-password"
if defaults {
username, password = "admin", "admin"
}
for name, value := range map[string]string{
"path": path, "username": username, "password": password,
"default": fmt.Sprint(defaults), "port": "54321", "x-ui": settingsCLIMock,
"writes": "", "path-writes": "", "restarts": "", "lifecycle": "", "x-ui.service": "test unit",
} {
if err := os.WriteFile(filepath.Join(dir, name), []byte(value), 0o700); err != nil {
t.Fatal(err)
}
}
function := "config_after_update"
if script == "install.sh" {
function = "config_after_install"
}
driver := `set -u
red= green= yellow= blue= plain=
xui_folder=$TEST_SETTINGS_DIR
xui_service=$TEST_SETTINGS_DIR
tag_version=test
script_ref=main
NONINTERACTIVE=1
release=ubuntu
SSL_SCHEME=https
SSL_HOST=panel.example
curl() { printf '203.0.113.10\n200\n'; }
systemctl() { printf '%s\n' "$*" >> "$TEST_SETTINGS_DIR/restarts"; }
rc-service() { printf '%s\n' "$*" >> "$TEST_SETTINGS_DIR/restarts"; }
prompt_and_setup_ssl() { printf '%s\n' "$*" > "$TEST_SETTINGS_DIR/ssl"; }
write_install_result() { printf '%s\n' "$@" > "$TEST_SETTINGS_DIR/result"; }
install_postgres_local() { echo 'unexpected PostgreSQL installation' >&2; exit 97; }
chown() { :; }
chmod() { :; }
[() { if [[ "${2:-}" == /etc/* ]]; then return 1; fi; builtin [ "$@"; }
_install_xui_service_unit() { printf 'unit\n' >> "$TEST_SETTINGS_DIR/lifecycle"; }
setup_fail2ban() { printf 'fail2ban\n' >> "$TEST_SETTINGS_DIR/lifecycle"; }
`
driver += scriptFunction(t, script, "gen_random_string") + scriptFunction(t, script, function)
if script == "install.sh" {
for _, helper := range []string{"is_port_in_use", "normalize_panel_port", "select_panel_port"} {
driver += scriptFunction(t, script, helper)
}
}
if continueSetup {
parent := "update_x-ui"
if script == "install.sh" {
parent = "install_x-ui"
}
body := scriptFunction(t, script, parent)
start := strings.Index(body, "\n "+function)
if start < 0 {
t.Fatalf("%s: missing configuration call in %s", script, parent)
}
driver += "finish_setup() {" + body[start:] + "finish_setup\n"
} else {
driver += fmt.Sprintf("for ((i=0; i<%d; i++)); do %s || exit $?; done\n", runs, function)
}
cmd := exec.Command(bash, "-c", driver)
cmd.Dir = dir
for _, value := range os.Environ() {
if !strings.HasPrefix(value, "XUI_") && !strings.HasPrefix(value, "TEST_") {
cmd.Env = append(cmd.Env, value)
}
}
cmd.Env = append(cmd.Env, "TEST_SETTINGS_DIR="+dir, "TEST_READ_MODE="+mode,
"XUI_DB_TYPE=sqlite", "XUI_PANEL_PORT=41234")
out, err := cmd.CombinedOutput()
return dir, string(out), err
}
func readScriptState(t *testing.T, dir, name string) string {
t.Helper()
value, err := os.ReadFile(filepath.Join(dir, name))
if err != nil {
t.Fatal(err)
}
return string(value)
}
func TestScriptConfigPreservesCustomBasePaths(t *testing.T) {
for _, script := range []string{"install.sh", "update.sh"} {
for _, path := range []string{"/", "", "/a/", "/ab/", "/abc/", "/mypanel/", "/failed/", "/my panel/"} {
for _, defaults := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/path=%q/defaults=%t", script, path, defaults), func(t *testing.T) {
dir, out, err := runScriptConfig(t, script, path, defaults, "", 2, false)
if err != nil {
t.Fatalf("configure twice: %v\n%s", err, out)
}
actualPath := readScriptState(t, dir, "path")
initialize := path == "" || path == "/"
if initialize {
if !regexp.MustCompile(`^/[A-Za-z0-9]{18}/$`).MatchString(actualPath) {
t.Errorf("initialized path = %q, want secure 18-character path", actualPath)
}
if n := strings.Count(readScriptState(t, dir, "path-writes"), "\n"); n != 1 {
t.Errorf("path initialized %d times across two runs, want once", n)
}
} else if actualPath != path || readScriptState(t, dir, "path-writes") != "" {
t.Errorf("custom path %q became %q; writes=%q", path, actualPath, readScriptState(t, dir, "path-writes"))
}
for _, field := range []string{"username", "password"} {
actual := readScriptState(t, dir, field)
if defaults && script == "install.sh" {
if !regexp.MustCompile(`^[A-Za-z0-9]{10}$`).MatchString(actual) || actual == "admin" {
t.Errorf("%s was not securely initialized: %q", field, actual)
}
} else {
want := "admin"
if !defaults {
want = "custom-" + field
if field == "username" {
want = "custom-user"
}
}
if actual != want {
t.Errorf("%s = %q, want preserved %q", field, actual, want)
}
}
}
wantRestarts := ""
if initialize && script == "update.sh" {
wantRestarts = "restart x-ui\n"
}
if actual := readScriptState(t, dir, "restarts"); actual != wantRestarts {
t.Errorf("path-related restarts = %q, want %q", actual, wantRestarts)
}
wantWrites := 0
if initialize || (defaults && script == "install.sh") {
wantWrites = 1
}
if n := strings.Count(readScriptState(t, dir, "writes"), "\n"); n != wantWrites {
t.Errorf("configuration writes = %d, want %d across two runs", n, wantWrites)
}
wantPort := "54321"
if initialize && defaults && script == "install.sh" {
wantPort = "41234"
}
if actual := readScriptState(t, dir, "port"); actual != wantPort {
t.Errorf("port = %q, want %q", actual, wantPort)
}
if !initialize {
if _, err := os.Stat(filepath.Join(dir, "ssl")); !os.IsNotExist(err) {
t.Errorf("configured TLS should not be reinitialized: %v", err)
}
}
})
}
}
}
}
func TestScriptConfigRejectsUnreadableSettings(t *testing.T) {
for _, script := range []string{"install.sh", "update.sh"} {
for _, mode := range []string{"failure", "empty", "missing_field", "malformed", "duplicate", "diagnostic"} {
t.Run(script+"/"+mode, func(t *testing.T) {
dir, out, err := runScriptConfig(t, script, "/", true, mode, 1, false)
if err == nil || !strings.Contains(out, "Unable to read webBasePath") {
t.Errorf("settings read should stop with an actionable error; got %v\n%s", err, out)
}
for name, want := range map[string]string{"path": "/", "username": "admin", "password": "admin", "writes": "", "restarts": ""} {
if actual := readScriptState(t, dir, name); actual != want {
t.Errorf("failed settings read changed %s to %q, want %q", name, actual, want)
}
}
})
}
}
}
func TestScriptConfigReadFailureKeepsServiceSetup(t *testing.T) {
for _, script := range []string{"install.sh", "update.sh"} {
t.Run(script, func(t *testing.T) {
dir, out, err := runScriptConfig(t, script, "/a/", true, "failure", 1, true)
if err != nil || !strings.Contains(out, "Unable to read webBasePath") {
t.Errorf("service setup should continue after the settings warning: %v\n%s", err, out)
}
wantLifecycle, wantServices := "fail2ban\n", ""
if script == "install.sh" {
wantLifecycle = "unit\nfail2ban\n"
wantServices = "daemon-reload\nenable x-ui\nstart x-ui\n"
}
for name, want := range map[string]string{
"path": "/a/", "username": "admin", "password": "admin", "writes": "",
"lifecycle": wantLifecycle, "restarts": wantServices,
} {
if actual := readScriptState(t, dir, name); actual != want {
t.Errorf("%s = %q, want %q", name, actual, want)
}
}
})
}
}