test: Add tests for getInstanceConfig, baseIntervalForInstanceType, LoadOIDCConfig

- getInstanceConfig: 33%→100% (nil handling, case-insensitive matching)
- baseIntervalForInstanceType: 50%→100% (all instance types, clamping)
- LoadOIDCConfig: 35%→94% (file not exist, read errors, decryption)
This commit is contained in:
rcourtman 2025-12-01 18:06:15 +00:00
parent 4defced1a6
commit 0803951854
2 changed files with 447 additions and 0 deletions

View file

@ -1487,3 +1487,223 @@ func TestLoadSystemSettingsFileNotExist(t *testing.T) {
t.Fatal("expected nil for non-existent system settings file")
}
}
// ============================================================================
// LoadOIDCConfig error paths and success cases
// ============================================================================
func TestLoadOIDCConfigFileNotExist(t *testing.T) {
tempDir := t.TempDir()
cp := config.NewConfigPersistence(tempDir)
if err := cp.EnsureConfigDir(); err != nil {
t.Fatalf("EnsureConfigDir: %v", err)
}
// Don't create the file - test that nil, nil is returned
cfg, err := cp.LoadOIDCConfig()
if err != nil {
t.Fatalf("LoadOIDCConfig returned error for non-existent file: %v", err)
}
if cfg != nil {
t.Fatal("expected nil config for non-existent file, got non-nil")
}
}
func TestLoadOIDCConfigFileReadError(t *testing.T) {
tempDir := t.TempDir()
cp := config.NewConfigPersistence(tempDir)
if err := cp.EnsureConfigDir(); err != nil {
t.Fatalf("EnsureConfigDir: %v", err)
}
// Create oidc.enc as a directory to trigger a read error (not IsNotExist)
oidcFile := filepath.Join(tempDir, "oidc.enc")
if err := os.Mkdir(oidcFile, 0700); err != nil {
t.Fatalf("Mkdir: %v", err)
}
_, err := cp.LoadOIDCConfig()
if err == nil {
t.Fatal("expected error when reading directory as file, got nil")
}
}
func TestLoadOIDCConfigValidJSONWithoutEncryption(t *testing.T) {
tempDir := t.TempDir()
cp := config.NewConfigPersistence(tempDir)
if err := cp.EnsureConfigDir(); err != nil {
t.Fatalf("EnsureConfigDir: %v", err)
}
// Use SaveOIDCConfig to properly save (and encrypt) the config,
// then LoadOIDCConfig should be able to load it back
expected := config.OIDCConfig{
Enabled: true,
IssuerURL: "https://auth.example.com",
ClientID: "test-client-id",
ClientSecret: "test-client-secret",
RedirectURL: "https://app.example.com/callback",
Scopes: []string{"openid", "email", "profile"},
UsernameClaim: "preferred_username",
}
if err := cp.SaveOIDCConfig(expected); err != nil {
t.Fatalf("SaveOIDCConfig: %v", err)
}
loaded, err := cp.LoadOIDCConfig()
if err != nil {
t.Fatalf("LoadOIDCConfig: %v", err)
}
if loaded == nil {
t.Fatal("expected non-nil config, got nil")
}
if loaded.Enabled != expected.Enabled {
t.Fatalf("Enabled mismatch: got %v want %v", loaded.Enabled, expected.Enabled)
}
if loaded.IssuerURL != expected.IssuerURL {
t.Fatalf("IssuerURL mismatch: got %q want %q", loaded.IssuerURL, expected.IssuerURL)
}
if loaded.ClientID != expected.ClientID {
t.Fatalf("ClientID mismatch: got %q want %q", loaded.ClientID, expected.ClientID)
}
if loaded.ClientSecret != expected.ClientSecret {
t.Fatalf("ClientSecret mismatch: got %q want %q", loaded.ClientSecret, expected.ClientSecret)
}
if loaded.RedirectURL != expected.RedirectURL {
t.Fatalf("RedirectURL mismatch: got %q want %q", loaded.RedirectURL, expected.RedirectURL)
}
if loaded.UsernameClaim != expected.UsernameClaim {
t.Fatalf("UsernameClaim mismatch: got %q want %q", loaded.UsernameClaim, expected.UsernameClaim)
}
if !reflect.DeepEqual(loaded.Scopes, expected.Scopes) {
t.Fatalf("Scopes mismatch: got %v want %v", loaded.Scopes, expected.Scopes)
}
}
func TestLoadOIDCConfigInvalidJSON(t *testing.T) {
tempDir := t.TempDir()
cp := config.NewConfigPersistence(tempDir)
if err := cp.EnsureConfigDir(); err != nil {
t.Fatalf("EnsureConfigDir: %v", err)
}
// Write invalid data to oidc.enc file
// Since crypto is enabled, writing raw JSON will cause a decryption error first
// To test JSON unmarshal error, we need to bypass encryption or test the path
// where crypto is nil. Writing garbage data will trigger decrypt error which
// covers the decrypt error path.
oidcFile := filepath.Join(tempDir, "oidc.enc")
if err := os.WriteFile(oidcFile, []byte(`{invalid json content`), 0600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
_, err := cp.LoadOIDCConfig()
if err == nil {
t.Fatal("expected error for invalid data, got nil")
}
}
func TestLoadOIDCConfigDecryptionError(t *testing.T) {
tempDir := t.TempDir()
cp := config.NewConfigPersistence(tempDir)
if err := cp.EnsureConfigDir(); err != nil {
t.Fatalf("EnsureConfigDir: %v", err)
}
// Write data that will fail decryption (not valid encrypted format)
oidcFile := filepath.Join(tempDir, "oidc.enc")
if err := os.WriteFile(oidcFile, []byte(`corrupted encrypted data`), 0600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
_, err := cp.LoadOIDCConfig()
if err == nil {
t.Fatal("expected decryption error, got nil")
}
}
func TestLoadOIDCConfigRoundTrip(t *testing.T) {
tempDir := t.TempDir()
cp := config.NewConfigPersistence(tempDir)
if err := cp.EnsureConfigDir(); err != nil {
t.Fatalf("EnsureConfigDir: %v", err)
}
// Test full round-trip with encryption
original := config.OIDCConfig{
Enabled: true,
IssuerURL: "https://idp.example.org/realms/myrealm",
ClientID: "my-app",
ClientSecret: "super-secret-value",
RedirectURL: "https://myapp.example.org/auth/callback",
LogoutURL: "https://idp.example.org/realms/myrealm/protocol/openid-connect/logout",
Scopes: []string{"openid", "email", "profile", "groups"},
UsernameClaim: "preferred_username",
EmailClaim: "email",
GroupsClaim: "groups",
AllowedGroups: []string{"admin", "users"},
AllowedDomains: []string{"example.org"},
AllowedEmails: []string{"admin@example.org"},
CABundle: "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----",
}
if err := cp.SaveOIDCConfig(original); err != nil {
t.Fatalf("SaveOIDCConfig: %v", err)
}
loaded, err := cp.LoadOIDCConfig()
if err != nil {
t.Fatalf("LoadOIDCConfig: %v", err)
}
if loaded == nil {
t.Fatal("expected config, got nil")
}
// Verify all fields are preserved through the round-trip
if loaded.Enabled != original.Enabled {
t.Errorf("Enabled: got %v want %v", loaded.Enabled, original.Enabled)
}
if loaded.IssuerURL != original.IssuerURL {
t.Errorf("IssuerURL: got %q want %q", loaded.IssuerURL, original.IssuerURL)
}
if loaded.ClientID != original.ClientID {
t.Errorf("ClientID: got %q want %q", loaded.ClientID, original.ClientID)
}
if loaded.ClientSecret != original.ClientSecret {
t.Errorf("ClientSecret: got %q want %q", loaded.ClientSecret, original.ClientSecret)
}
if loaded.RedirectURL != original.RedirectURL {
t.Errorf("RedirectURL: got %q want %q", loaded.RedirectURL, original.RedirectURL)
}
if loaded.LogoutURL != original.LogoutURL {
t.Errorf("LogoutURL: got %q want %q", loaded.LogoutURL, original.LogoutURL)
}
if !reflect.DeepEqual(loaded.Scopes, original.Scopes) {
t.Errorf("Scopes: got %v want %v", loaded.Scopes, original.Scopes)
}
if loaded.UsernameClaim != original.UsernameClaim {
t.Errorf("UsernameClaim: got %q want %q", loaded.UsernameClaim, original.UsernameClaim)
}
if loaded.EmailClaim != original.EmailClaim {
t.Errorf("EmailClaim: got %q want %q", loaded.EmailClaim, original.EmailClaim)
}
if loaded.GroupsClaim != original.GroupsClaim {
t.Errorf("GroupsClaim: got %q want %q", loaded.GroupsClaim, original.GroupsClaim)
}
if !reflect.DeepEqual(loaded.AllowedGroups, original.AllowedGroups) {
t.Errorf("AllowedGroups: got %v want %v", loaded.AllowedGroups, original.AllowedGroups)
}
if !reflect.DeepEqual(loaded.AllowedDomains, original.AllowedDomains) {
t.Errorf("AllowedDomains: got %v want %v", loaded.AllowedDomains, original.AllowedDomains)
}
if !reflect.DeepEqual(loaded.AllowedEmails, original.AllowedEmails) {
t.Errorf("AllowedEmails: got %v want %v", loaded.AllowedEmails, original.AllowedEmails)
}
if loaded.CABundle != original.CABundle {
t.Errorf("CABundle: got %q want %q", loaded.CABundle, original.CABundle)
}
}

View file

@ -3,6 +3,8 @@ package monitoring
import (
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
)
func TestParseDurationEnv(t *testing.T) {
@ -162,3 +164,228 @@ func TestParseIntEnv(t *testing.T) {
}
})
}
func TestGetInstanceConfig(t *testing.T) {
t.Run("nil Monitor returns nil", func(t *testing.T) {
var m *Monitor
result := m.getInstanceConfig("any")
if result != nil {
t.Errorf("expected nil, got %+v", result)
}
})
t.Run("nil config returns nil", func(t *testing.T) {
m := &Monitor{config: nil}
result := m.getInstanceConfig("any")
if result != nil {
t.Errorf("expected nil, got %+v", result)
}
})
t.Run("empty PVEInstances slice returns nil", func(t *testing.T) {
m := &Monitor{
config: &config.Config{
PVEInstances: []config.PVEInstance{},
},
}
result := m.getInstanceConfig("any")
if result != nil {
t.Errorf("expected nil, got %+v", result)
}
})
t.Run("instance found by exact name match", func(t *testing.T) {
m := &Monitor{
config: &config.Config{
PVEInstances: []config.PVEInstance{
{Name: "pve1", Host: "192.168.1.1"},
},
},
}
result := m.getInstanceConfig("pve1")
if result == nil {
t.Fatal("expected non-nil result")
}
if result.Name != "pve1" {
t.Errorf("expected Name 'pve1', got '%s'", result.Name)
}
if result.Host != "192.168.1.1" {
t.Errorf("expected Host '192.168.1.1', got '%s'", result.Host)
}
})
t.Run("instance found by case-insensitive match", func(t *testing.T) {
m := &Monitor{
config: &config.Config{
PVEInstances: []config.PVEInstance{
{Name: "pve1", Host: "192.168.1.1"},
},
},
}
result := m.getInstanceConfig("PVE1")
if result == nil {
t.Fatal("expected non-nil result")
}
if result.Name != "pve1" {
t.Errorf("expected Name 'pve1', got '%s'", result.Name)
}
})
t.Run("instance not found returns nil", func(t *testing.T) {
m := &Monitor{
config: &config.Config{
PVEInstances: []config.PVEInstance{
{Name: "pve1", Host: "192.168.1.1"},
},
},
}
result := m.getInstanceConfig("nonexistent")
if result != nil {
t.Errorf("expected nil, got %+v", result)
}
})
t.Run("multiple instances finds correct one", func(t *testing.T) {
m := &Monitor{
config: &config.Config{
PVEInstances: []config.PVEInstance{
{Name: "pve1", Host: "192.168.1.1"},
{Name: "pve2", Host: "192.168.1.2"},
{Name: "pve3", Host: "192.168.1.3"},
},
},
}
result := m.getInstanceConfig("pve2")
if result == nil {
t.Fatal("expected non-nil result")
}
if result.Name != "pve2" {
t.Errorf("expected Name 'pve2', got '%s'", result.Name)
}
if result.Host != "192.168.1.2" {
t.Errorf("expected Host '192.168.1.2', got '%s'", result.Host)
}
})
}
func TestBaseIntervalForInstanceType(t *testing.T) {
defaultInterval := DefaultSchedulerConfig().BaseInterval
t.Run("nil Monitor returns default", func(t *testing.T) {
var m *Monitor
result := m.baseIntervalForInstanceType(InstanceTypePVE)
if result != defaultInterval {
t.Errorf("expected %v, got %v", defaultInterval, result)
}
})
t.Run("nil config returns default", func(t *testing.T) {
m := &Monitor{config: nil}
result := m.baseIntervalForInstanceType(InstanceTypePVE)
if result != defaultInterval {
t.Errorf("expected %v, got %v", defaultInterval, result)
}
})
t.Run("InstanceTypePVE returns effectivePVEPollingInterval result", func(t *testing.T) {
m := &Monitor{
config: &config.Config{
PVEPollingInterval: 30 * time.Second,
},
}
result := m.baseIntervalForInstanceType(InstanceTypePVE)
expected := 30 * time.Second
if result != expected {
t.Errorf("expected %v, got %v", expected, result)
}
})
t.Run("InstanceTypePBS returns clamped PBS interval", func(t *testing.T) {
m := &Monitor{
config: &config.Config{
PBSPollingInterval: 45 * time.Second,
},
}
result := m.baseIntervalForInstanceType(InstanceTypePBS)
expected := 45 * time.Second
if result != expected {
t.Errorf("expected %v, got %v", expected, result)
}
})
t.Run("InstanceTypePBS with interval < 10s gets clamped to 10s", func(t *testing.T) {
m := &Monitor{
config: &config.Config{
PBSPollingInterval: 5 * time.Second,
},
}
result := m.baseIntervalForInstanceType(InstanceTypePBS)
expected := 10 * time.Second
if result != expected {
t.Errorf("expected %v, got %v", expected, result)
}
})
t.Run("InstanceTypePBS with interval > 1h gets clamped to 1h", func(t *testing.T) {
m := &Monitor{
config: &config.Config{
PBSPollingInterval: 2 * time.Hour,
},
}
result := m.baseIntervalForInstanceType(InstanceTypePBS)
expected := time.Hour
if result != expected {
t.Errorf("expected %v, got %v", expected, result)
}
})
t.Run("InstanceTypePMG returns clamped PMG interval", func(t *testing.T) {
m := &Monitor{
config: &config.Config{
PMGPollingInterval: 2 * time.Minute,
},
}
result := m.baseIntervalForInstanceType(InstanceTypePMG)
expected := 2 * time.Minute
if result != expected {
t.Errorf("expected %v, got %v", expected, result)
}
})
t.Run("unknown instance type with positive AdaptivePollingBaseInterval", func(t *testing.T) {
m := &Monitor{
config: &config.Config{
AdaptivePollingBaseInterval: 20 * time.Second,
},
}
result := m.baseIntervalForInstanceType(InstanceType("unknown"))
expected := 20 * time.Second
if result != expected {
t.Errorf("expected %v, got %v", expected, result)
}
})
t.Run("unknown instance type with zero AdaptivePollingBaseInterval uses default", func(t *testing.T) {
m := &Monitor{
config: &config.Config{
AdaptivePollingBaseInterval: 0,
},
}
result := m.baseIntervalForInstanceType(InstanceType("unknown"))
if result != defaultInterval {
t.Errorf("expected %v, got %v", defaultInterval, result)
}
})
t.Run("unknown instance type with negative AdaptivePollingBaseInterval uses default", func(t *testing.T) {
m := &Monitor{
config: &config.Config{
AdaptivePollingBaseInterval: -5 * time.Second,
},
}
result := m.baseIntervalForInstanceType(InstanceType("unknown"))
if result != defaultInterval {
t.Errorf("expected %v, got %v", defaultInterval, result)
}
})
}