install_compose_test.go
Rama main · réplica de lectura del repositorio original.
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))
}