Pulse/internal/config/config_load_test.go
2026-03-18 16:06:30 +00:00

333 lines
8.4 KiB
Go

package config
import (
"encoding/base64"
"os"
"path/filepath"
"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 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_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_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)
}