Ctrl K
ComunidadGuías…

install_compose_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package main

import (
	"os"
	"os/exec"
	"strings"
	"testing"

	"github.com/router-for-me/CLIProxyAPI/v6/internal/identity"
)

// These are configuration drift guard tests: they assert generated install
// script text, not runtime behavior.
func TestInstallEnvProvidesHostAbsoluteBindPaths(t *testing.T) {
	data, err := os.ReadFile("install.sh")
	if err != nil {
		t.Fatalf("read install.sh: %v", err)
	}
	content := string(data)

	for _, want := range []string{
		"CLI_PROXY_CONFIG_PATH=${INSTALL_DIR}/config.yaml",
		"CLI_PROXY_AUTH_PATH=${INSTALL_DIR}/auths",
		"AUTH_PATH=/root/.cli-proxy-api",
		"CLI_PROXY_LOG_PATH=${INSTALL_DIR}/logs",
		"CLI_PROXY_DATA_PATH=${INSTALL_DIR}/data",
	} {
		if !strings.Contains(content, want) {
			t.Fatalf("install.sh missing %q", want)
		}
	}
}

func TestInstallComposeUsesHostPathVariablesForDataMounts(t *testing.T) {
	data, err := os.ReadFile("install.sh")
	if err != nil {
		t.Fatalf("read install.sh: %v", err)
	}
	content := string(data)

	for _, want := range []string{
		"${CLI_PROXY_CONFIG_PATH}:/CLIProxyAPI/config.yaml",
		"${CLI_PROXY_AUTH_PATH}:${AUTH_PATH}",
		"${CLI_PROXY_LOG_PATH}:/CLIProxyAPI/logs",
		"${CLI_PROXY_DATA_PATH}:/CLIProxyAPI/data",
		"AUTH_PATH: ${AUTH_PATH}",
	} {
		if !strings.Contains(content, want) {
			t.Fatalf("install.sh generated compose missing %q", want)
		}
	}

	for _, forbidden := range []string{
		"./config.yaml:/CLIProxyAPI/config.yaml",
		"./auths:/root/.cli-proxy-api",
		"./logs:/CLIProxyAPI/logs",
		"./data:/CLIProxyAPI/data",
	} {
		if strings.Contains(content, forbidden) {
			t.Fatalf("install.sh generated compose still contains relative bind mount %q", forbidden)
		}
	}
}

func TestInstallComposeMirrorsDeploymentFilesAtHostPathInUpdater(t *testing.T) {
	data, err := os.ReadFile("install.sh")
	if err != nil {
		t.Fatalf("read install.sh: %v", err)
	}
	content := string(data)
	updaterStart := strings.Index(content, "  clirelay-updater:\n")
	updaterEnd := strings.Index(content, "\n  postgres:")
	if updaterStart < 0 || updaterEnd <= updaterStart {
		t.Fatal("install.sh generated compose missing clirelay-updater service block")
	}
	updater := content[updaterStart:updaterEnd]

	for _, want := range []string{
		"CLIRELAY_COMPOSE_FILE: ${CLIRELAY_INSTALL_DIR}/docker-compose.yml",
		"CLIRELAY_ENV_FILE: ${CLIRELAY_INSTALL_DIR}/.env",
		"CLIRELAY_UPDATER_STATE_FILE: ${CLIRELAY_INSTALL_DIR}/.clirelay-updater-status.json",
		".:${CLIRELAY_INSTALL_DIR}",
		"healthcheck:\n      disable: true",
	} {
		if !strings.Contains(updater, want) {
			t.Fatalf("install.sh generated updater compose missing %q", want)
		}
	}

	for _, forbidden := range []string{
		"CLIRELAY_COMPOSE_FILE: /workspace/docker-compose.yml",
		"CLIRELAY_ENV_FILE: /workspace/.env",
		"./docker-compose.yml:/workspace/docker-compose.yml:ro",
		"./.env:/workspace/.env",
		"./docker-compose.yml:${CLIRELAY_INSTALL_DIR}/docker-compose.yml:ro",
	} {
		if strings.Contains(content, forbidden) {
			t.Fatalf("install.sh generated updater compose still contains /workspace mapping %q", forbidden)
		}
	}
}

func TestInstallComposeIncludesRuntimeDataStack(t *testing.T) {
	data, err := os.ReadFile("install.sh")
	if err != nil {
		t.Fatalf("read install.sh: %v", err)
	}
	content := string(data)

	for _, want := range []string{
		`updater_token="${CLIRELAY_UPDATER_TOKEN:-$(rand_hex 16)}"`,
		`postgres_db="${CLIRELAY_POSTGRES_DB:-cliproxy}"`,
		`postgres_user="${CLIRELAY_POSTGRES_USER:-cliproxy}"`,
		`postgres_password="${CLIRELAY_POSTGRES_PASSWORD:-$(rand_hex 16)}"`,
		`postgres_dsn="${CLIRELAY_POSTGRES_DSN:-postgres://${postgres_user}:${postgres_password}@postgres:5432/${postgres_db}?sslmode=disable}"`,
		`redis_addr="${CLIRELAY_REDIS_ADDR:-redis:6379}"`,
		`redis_db="${CLIRELAY_REDIS_DB:-0}"`,
		"CLIRELAY_UPDATER_TOKEN=${updater_token}",
		"CLIRELAY_POSTGRES_DB=${postgres_db}",
		"CLIRELAY_POSTGRES_USER=${postgres_user}",
		"CLIRELAY_POSTGRES_DSN=${postgres_dsn}",
		"CLIRELAY_REDIS_ENABLE=true",
		"CLIRELAY_REDIS_ADDR=${redis_addr}",
		"CLIRELAY_REDIS_DB=${redis_db}",
		"CLIRELAY_POSTGRES_DATA_PATH=${postgres_data_path}",
		"CLIRELAY_REDIS_DATA_PATH=${redis_data_path}",
		"CLIRELAY_POSTGRES_DSN: ${CLIRELAY_POSTGRES_DSN}",
		"CLIRELAY_REDIS_ENABLE: ${CLIRELAY_REDIS_ENABLE}",
		"postgres:\n    image: postgres:15-alpine",
		"redis:\n    image: redis:7-alpine",
		"depends_on:\n      postgres:\n        condition: service_healthy\n      redis:\n        condition: service_healthy",
	} {
		if !strings.Contains(content, want) {
			t.Fatalf("install.sh missing runtime data stack text %q", want)
		}
	}
}

func TestInstallEnvGeneratesBootstrapAdminPassword(t *testing.T) {
	data, err := os.ReadFile("install.sh")
	if err != nil {
		t.Fatalf("read install.sh: %v", err)
	}
	content := string(data)

	// Without a generated admin password the identity bootstrap falls back to the
	// empty remote-management.secret-key and a fresh install crash-loops.
	for _, want := range []string{
		`CFG_ADMIN_PASSWORD="$(resolve_admin_password)"`,
		"CLIRELAY_ADMIN_PASSWORD=${admin_password}",
		"CLIRELAY_ADMIN_PASSWORD: ${CLIRELAY_ADMIN_PASSWORD}",
		// The generated credential has to be reported, or the operator cannot sign in.
		"Panel login : admin /",
	} {
		if !strings.Contains(content, want) {
			t.Fatalf("install.sh missing %q", want)
		}
	}
}

// Executes the password generator defined in install.sh and checks the result against
// the same validation the bootstrap applies, so a hex-only generator cannot regress.
func TestInstallRandPasswordPassesIdentityValidation(t *testing.T) {
	script := `set -euo pipefail
` + extractShellFunctions(t, "install.sh", "rand_hex", "rand_password") + `
rand_password 16`

	out, err := exec.Command("bash", "-c", script).Output()
	if err != nil {
		t.Fatalf("run install.sh rand_password: %v", err)
	}
	password := strings.TrimSpace(string(out))
	if password == "" {
		t.Fatal("rand_password produced an empty value")
	}
	if _, err := identity.HashPassword(password); err != nil {
		t.Fatalf("install.sh rand_password produced %q, rejected by identity.HashPassword: %v", password, err)
	}
}

// extractShellFunctions pulls top-level `name() { ... }` blocks out of a shell script so
// a test can run them without executing the whole installer.
func extractShellFunctions(t *testing.T, path string, names ...string) string {
	t.Helper()
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read %s: %v", path, err)
	}
	lines := strings.Split(string(data), "\n")
	var out []string
	for _, name := range names {
		header := name + "() {"
		start := -1
		for i, line := range lines {
			if strings.TrimSpace(line) == header {
				start = i
				break
			}
		}
		if start < 0 {
			t.Fatalf("%s does not define %s()", path, name)
		}
		end := -1
		for i := start + 1; i < len(lines); i++ {
			if lines[i] == "}" {
				end = i
				break
			}
		}
		if end < 0 {
			t.Fatalf("%s: unterminated %s()", path, name)
		}
		out = append(out, strings.Join(lines[start:end+1], "\n"))
	}
	return strings.Join(out, "\n")
}

// The bootstrap admin password reuses the management secret when that secret satisfies
// the identity policy, so the operator keeps the one credential they were shown. This
// exercises the real shell functions rather than asserting on their source text.
func TestInstallResolveAdminPasswordPrefersCompliantManagementSecret(t *testing.T) {
	fns := extractShellFunctions(t, "install.sh", "rand_hex", "rand_password", "password_is_valid", "resolve_admin_password")

	compliant := runShell(t, fns+"\nCFG_SECRET='MyStr0ng!Secret'\nresolve_admin_password")
	if compliant != "MyStr0ng!Secret" {
		t.Fatalf("compliant secret was not reused: %q", compliant)
	}

	generated := runShell(t, fns+"\nCFG_SECRET='weak'\nresolve_admin_password")
	if generated == "weak" {
		t.Fatal("a secret that fails the password policy was reused as the admin password")
	}
	if _, err := identity.HashPassword(generated); err != nil {
		t.Fatalf("generated admin password %q is rejected: %v", generated, err)
	}
}

// The auto-generated management secret doubles as the admin password, so it too must
// satisfy the policy.
func TestInstallGeneratedManagementSecretSatisfiesPasswordPolicy(t *testing.T) {
	fns := extractShellFunctions(t, "install.sh", "rand_hex", "rand_password")
	secret := runShell(t, fns+"\nrand_password 16")
	if _, err := identity.HashPassword(secret); err != nil {
		t.Fatalf("generated management secret %q is rejected as an admin password: %v", secret, err)
	}
}

func runShell(t *testing.T, script string) string {
	t.Helper()
	out, err := exec.Command("bash", "-c", "set -euo pipefail\n"+script).Output()
	if err != nil {
		t.Fatalf("run shell snippet: %v", err)
	}
	return strings.TrimSpace(string(out))
}