package config import ( "encoding/base64" "errors" "os" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestLoad_Defaults(t *testing.T) { // Avoid relying on /etc/pulse existing on the machine running tests. // We still want to verify "defaults" behavior when PULSE_DATA_DIR is unset. tmpDefault := t.TempDir() prevDefault := defaultDataDir defaultDataDir = tmpDefault t.Cleanup(func() { defaultDataDir = prevDefault }) // Clear env vars that might affect defaults os.Unsetenv("PULSE_DATA_DIR") os.Unsetenv("FRONTEND_PORT") cfg, err := Load() require.NoError(t, err) assert.Equal(t, 7655, cfg.FrontendPort) assert.Equal(t, tmpDefault, cfg.DataPath) } func TestLoad_DefaultHostedCommercialBaseURLAvoidsRetiredTrialPath(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) t.Setenv("PULSE_PRO_TRIAL_SIGNUP_URL", "") cfg, err := Load() require.NoError(t, err) if strings.Contains(cfg.ProTrialSignupURL, "start-pro-trial") { t.Fatalf("ProTrialSignupURL=%q must not default to retired trial signup route", cfg.ProTrialSignupURL) } } func TestResolveRuntimeDataDir(t *testing.T) { tmpDefault := t.TempDir() prevDefault := defaultDataDir defaultDataDir = tmpDefault t.Cleanup(func() { defaultDataDir = prevDefault }) t.Run("explicit_path_wins", func(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) explicit := t.TempDir() assert.Equal(t, explicit, ResolveRuntimeDataDir(explicit)) }) t.Run("env_path_fallback", func(t *testing.T) { envDir := t.TempDir() t.Setenv("PULSE_DATA_DIR", envDir) assert.Equal(t, envDir, ResolveRuntimeDataDir("")) }) t.Run("default_fallback", func(t *testing.T) { os.Unsetenv("PULSE_DATA_DIR") assert.Equal(t, tmpDefault, ResolveRuntimeDataDir("")) }) } func TestLoad_EnvOverrides(t *testing.T) { // Set some env vars t.Setenv("FRONTEND_PORT", "8080") tempDir := t.TempDir() t.Setenv("PULSE_DATA_DIR", tempDir) t.Setenv("HTTPS_ENABLED", "true") t.Setenv("PULSE_AUTH_USER", "admin") cfg, err := Load() require.NoError(t, err) assert.Equal(t, 8080, cfg.FrontendPort) assert.Equal(t, tempDir, cfg.DataPath) assert.True(t, cfg.HTTPSEnabled) assert.Equal(t, "admin", cfg.AuthUser) } func TestLoad_AgentIngestPortDefaultsDisabled(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) os.Unsetenv("PULSE_AGENT_INGEST_PORT") cfg, err := Load() require.NoError(t, err) assert.Equal(t, 0, cfg.AgentIngestPort) } func TestLoad_AgentIngestPortEnvOverride(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) t.Setenv("PULSE_AGENT_INGEST_PORT", "7656") cfg, err := Load() require.NoError(t, err) assert.Equal(t, 7656, cfg.AgentIngestPort) } func TestLoad_AgentIngestPortInvalidValueIgnored(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) t.Setenv("PULSE_AGENT_INGEST_PORT", "not-a-port") cfg, err := Load() require.NoError(t, err) assert.Equal(t, 0, cfg.AgentIngestPort) } func TestLoad_AgentIngestPortRejectsFrontendPortCollision(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) t.Setenv("FRONTEND_PORT", "7655") t.Setenv("PULSE_AGENT_INGEST_PORT", "7655") _, err := Load() require.Error(t, err) assert.Contains(t, err.Error(), "agent ingest port") } func TestLoad_ProxmoxGuestDockerDetectionEnvOptIn(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) cfg, err := Load() require.NoError(t, err) assert.False(t, cfg.EnableProxmoxGuestDockerDetection) assert.False(t, cfg.EnableProxmoxGuestDockerInventory) assert.Empty(t, cfg.ProxmoxGuestDockerInventoryVMIDs) assert.False(t, cfg.EnvOverrides["PULSE_ENABLE_PROXMOX_GUEST_DOCKER_DETECTION"]) assert.False(t, cfg.EnvOverrides["PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY"]) assert.False(t, cfg.EnvOverrides["PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS"]) t.Setenv("PULSE_ENABLE_PROXMOX_GUEST_DOCKER_DETECTION", "true") t.Setenv("PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY", "true") t.Setenv("PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS", "101,102") cfg, err = Load() require.NoError(t, err) assert.True(t, cfg.EnableProxmoxGuestDockerDetection) assert.True(t, cfg.EnableProxmoxGuestDockerInventory) assert.Equal(t, "101,102", cfg.ProxmoxGuestDockerInventoryVMIDs) assert.True(t, cfg.EnvOverrides["PULSE_ENABLE_PROXMOX_GUEST_DOCKER_DETECTION"]) assert.True(t, cfg.EnvOverrides["PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY"]) assert.True(t, cfg.EnvOverrides["PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS"]) } func TestLoad_MetricsStorageEnvOverrides(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) t.Setenv("PULSE_METRICS_DB_PATH", "/dev/shm/pulse/metrics.db") t.Setenv("PULSE_METRICS_ROLLUP_INTERVAL", "30m") cfg, err := Load() require.NoError(t, err) assert.Equal(t, "/dev/shm/pulse/metrics.db", cfg.MetricsDBPath) assert.Equal(t, 30*time.Minute, cfg.MetricsRollupInterval) assert.True(t, cfg.EnvOverrides["PULSE_METRICS_DB_PATH"]) assert.True(t, cfg.EnvOverrides["PULSE_METRICS_ROLLUP_INTERVAL"]) } func TestLoad_MetricsBindSecurityDefaultsAndOverrides(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) cfg, err := Load() require.NoError(t, err) assert.Equal(t, "127.0.0.1", cfg.MetricsBindAddress) assert.False(t, cfg.MetricsAllowInsecureRemote) t.Setenv("PULSE_METRICS_BIND_ADDRESS", "0.0.0.0") t.Setenv("PULSE_METRICS_ALLOW_INSECURE_REMOTE", "true") cfg, err = Load() require.NoError(t, err) assert.Equal(t, "0.0.0.0", cfg.MetricsBindAddress) assert.True(t, cfg.MetricsAllowInsecureRemote) assert.True(t, cfg.EnvOverrides["PULSE_METRICS_BIND_ADDRESS"]) assert.True(t, cfg.EnvOverrides["PULSE_METRICS_ALLOW_INSECURE_REMOTE"]) } func TestLoad_InvalidMetricsRollupIntervalIgnored(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) t.Setenv("PULSE_METRICS_ROLLUP_INTERVAL", "2m") cfg, err := Load() require.NoError(t, err) assert.Zero(t, cfg.MetricsRollupInterval) assert.False(t, cfg.EnvOverrides["PULSE_METRICS_ROLLUP_INTERVAL"]) } func TestLoad_DotEnv(t *testing.T) { tempDir := t.TempDir() envFile := filepath.Join(tempDir, ".env") content := `PULSE_AUTH_USER="dotenvuser"` require.NoError(t, os.WriteFile(envFile, []byte(content), 0644)) t.Setenv("PULSE_DATA_DIR", tempDir) // Ensure no leakage os.Unsetenv("PULSE_AUTH_USER") cfg, err := Load() require.NoError(t, err) // godotenv.Load sets os env vars directly, bypassing t.Setenv cleanup t.Cleanup(func() { os.Unsetenv("PULSE_AUTH_USER") }) assert.Equal(t, "dotenvuser", cfg.AuthUser) } func TestLoad_APITokensEnvIgnored(t *testing.T) { os.Unsetenv("API_TOKEN") t.Setenv("API_TOKENS", "token1,token2") t.Setenv("PULSE_DATA_DIR", t.TempDir()) cfg, err := Load() require.NoError(t, err) assert.Len(t, cfg.APITokens, 0) assert.False(t, cfg.HasAPITokens()) } func TestLoad_LegacyAPITokenEnvIgnored(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) t.Setenv("API_TOKEN", "legacytoken") cfg, err := Load() require.NoError(t, err) assert.Len(t, cfg.APITokens, 0) assert.False(t, cfg.HasAPITokens()) } func TestLoad_APITokens_LegacyOrgBindingMigrated(t *testing.T) { tempDir := t.TempDir() t.Setenv("PULSE_DATA_DIR", tempDir) os.Unsetenv("API_TOKEN") os.Unsetenv("API_TOKENS") p := NewConfigPersistence(tempDir) legacy := []APITokenRecord{ { ID: "legacy-token", Name: "Legacy", Hash: "legacy-hash", Prefix: "legacy", Suffix: "hash", CreatedAt: time.Now().UTC(), Scopes: []string{ScopeWildcard}, }, } require.NoError(t, p.SaveAPITokens(legacy)) cfg, err := Load() require.NoError(t, err) require.Len(t, cfg.APITokens, 1) assert.Equal(t, "default", cfg.APITokens[0].OrgID) persisted, err := p.LoadAPITokens() require.NoError(t, err) require.Len(t, persisted, 1) assert.Equal(t, "default", persisted[0].OrgID) } func TestLoad_APITokens_MissingIDMigrated(t *testing.T) { tempDir := t.TempDir() t.Setenv("PULSE_DATA_DIR", tempDir) os.Unsetenv("API_TOKEN") os.Unsetenv("API_TOKENS") p := NewConfigPersistence(tempDir) legacy := []APITokenRecord{ { ID: "", Name: "Legacy Missing ID", Hash: "legacy-hash", Prefix: "legacy", Suffix: "hash", CreatedAt: time.Now().UTC(), Scopes: []string{ScopeWildcard}, OrgID: "default", }, } require.NoError(t, p.SaveAPITokens(legacy)) cfg, err := Load() require.NoError(t, err) require.Len(t, cfg.APITokens, 1) assert.NotEmpty(t, cfg.APITokens[0].ID) persisted, err := p.LoadAPITokens() require.NoError(t, err) require.Len(t, persisted, 1) assert.NotEmpty(t, persisted[0].ID) } func TestLoad_MockEnv(t *testing.T) { tempDir := t.TempDir() t.Setenv("PULSE_DATA_DIR", tempDir) require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".env"), []byte(`PULSE_MOCK_TEST="true"`), 0644)) t.Cleanup(func() { os.Unsetenv("PULSE_MOCK_TEST") }) _, err := Load() require.NoError(t, err) assert.Equal(t, "true", os.Getenv("PULSE_MOCK_TEST")) } func TestLoad_ProxyAuth(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) t.Setenv("PROXY_AUTH_SECRET", "secret") t.Setenv("PROXY_AUTH_USER_HEADER", "X-User") cfg, err := Load() require.NoError(t, err) assert.Equal(t, "secret", cfg.ProxyAuthSecret) assert.Equal(t, "X-User", cfg.ProxyAuthUserHeader) } func TestLoad_OIDCEnvIgnored(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) t.Setenv("OIDC_ENABLED", "true") t.Setenv("OIDC_ISSUER_URL", "https://issuer.com") t.Setenv("OIDC_CLIENT_ID", "client-id") t.Setenv("OIDC_CLIENT_SECRET", "client-secret") cfg, err := Load() require.NoError(t, err) require.NotNil(t, cfg) } func TestLoad_AuthPass_AutoHash(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) pass := "mysecretpassword" t.Setenv("PULSE_AUTH_PASS", pass) cfg, err := Load() require.NoError(t, err) assert.NotEqual(t, pass, cfg.AuthPass) assert.True(t, IsPasswordHashed(cfg.AuthPass)) } func TestLoad_AuthPass_PreHashed(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) hash := "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy" t.Setenv("PULSE_AUTH_PASS", hash) cfg, err := Load() require.NoError(t, err) assert.Equal(t, hash, cfg.AuthPass) } func TestLoad_AuthPass_HashFailureFailsClosed(t *testing.T) { t.Setenv("PULSE_DATA_DIR", t.TempDir()) t.Setenv("PULSE_AUTH_PASS", "plainpass") originalHashPasswordFn := hashPasswordFn hashPasswordFn = func(string) (string, error) { return "", errors.New("boom") } t.Cleanup(func() { hashPasswordFn = originalHashPasswordFn }) cfg, err := Load() require.Error(t, err) assert.Nil(t, cfg) assert.ErrorContains(t, err, "hash PULSE_AUTH_PASS") } func TestLoad_Persistence(t *testing.T) { tempDir := t.TempDir() t.Setenv("PULSE_DATA_DIR", tempDir) // 1. Create nodes.json using Persistence (handles encryption) p := NewConfigPersistence(tempDir) // nodes := NodesConfig{...} require.NoError(t, p.SaveNodesConfig( []PVEInstance{{Host: "https://pve1", TokenName: "t", TokenValue: "v"}}, nil, nil, )) // 2. Create system_settings.json sysContent := `{ "pvePollingInterval": 45, "logLevel": "debug" }` require.NoError(t, os.WriteFile(filepath.Join(tempDir, "system.json"), []byte(sysContent), 0644)) // Note: filename is system.json or system_settings.json? cfg, err := Load() require.NoError(t, err) // Debug: Check if path is correct assert.Equal(t, tempDir, cfg.ConfigPath) require.Len(t, cfg.PVEInstances, 1) assert.Equal(t, "https://pve1:8006", cfg.PVEInstances[0].Host) assert.Equal(t, 45*time.Second, cfg.PVEPollingInterval) assert.Equal(t, "debug", cfg.LogLevel) } func TestLoad_DisablesAutoUpdatesForRCChannel(t *testing.T) { tempDir := t.TempDir() t.Setenv("PULSE_DATA_DIR", tempDir) p := NewConfigPersistence(tempDir) require.NoError(t, p.SaveSystemSettings(SystemSettings{ UpdateChannel: "rc", AutoUpdateEnabled: true, })) cfg, err := Load() require.NoError(t, err) assert.Equal(t, "rc", cfg.UpdateChannel) assert.False(t, cfg.AutoUpdateEnabled) } func TestLoad_ReadErrors(t *testing.T) { if os.Getuid() == 0 { t.Skip("Skipping permission tests as root") } tempDir := t.TempDir() t.Setenv("PULSE_DATA_DIR", tempDir) // Create unreadable .env envFile := filepath.Join(tempDir, ".env") require.NoError(t, os.WriteFile(envFile, []byte("FOO=bar"), 0000)) // Create encryption key first (required before creating .enc files) key := make([]byte, 32) for i := range key { key[i] = byte(i) } encoded := base64.StdEncoding.EncodeToString(key) require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".encryption.key"), []byte(encoded), 0600)) // Create unreadable nodes.enc require.NoError(t, os.WriteFile(filepath.Join(tempDir, "nodes.enc"), []byte("data"), 0000)) // Load should warn but succeed with defaults cfg, err := Load() require.NoError(t, err) assert.NotNil(t, cfg) } func TestLoad_Persistence_InvalidFiles(t *testing.T) { tempDir := t.TempDir() t.Setenv("PULSE_DATA_DIR", tempDir) // Invalid JSON require.NoError(t, os.WriteFile(filepath.Join(tempDir, "nodes.json"), []byte("{invalid"), 0644)) cfg, err := Load() require.NoError(t, err) // Should not crash, just empty/defailts assert.Empty(t, cfg.PVEInstances) }