diff --git a/pkg/util/config/config_test.go b/pkg/util/config/config_test.go index eb27d1bda..c5fd48d93 100644 --- a/pkg/util/config/config_test.go +++ b/pkg/util/config/config_test.go @@ -2,10 +2,19 @@ package config import ( "fmt" + "os" "reflect" + "strings" "testing" ) +func TestMain(m *testing.M) { + // Set OPERATOR_NAMESPACE to avoid log.Fatal in GetOperatorNamespace + // when running tests outside a Kubernetes pod + os.Setenv("OPERATOR_NAMESPACE", "default") + os.Exit(m.Run()) +} + var getMapPairsFromStringTest = []struct { in string expected []string @@ -29,3 +38,300 @@ func TestGetMapPairsFromString(t *testing.T) { } } } + +func int32Ptr(i int32) *int32 { + return &i +} + +func boolPtr(b bool) *bool { + return &b +} + +var validateTests = []struct { + description string + cfg Config + expectError bool + errorMsg string +}{ + { + description: "valid config", + cfg: Config{ + Resources: Resources{ + MinInstances: 1, + MaxInstances: 5, + }, + Auth: Auth{ + SuperUsername: "postgres", + }, + ConnectionPooler: ConnectionPooler{ + NumberOfInstances: int32Ptr(2), + User: "pooler", + }, + Workers: 4, + }, + expectError: false, + }, + { + description: "min instances greater than max instances", + cfg: Config{ + Resources: Resources{ + MinInstances: 10, + MaxInstances: 5, + }, + Auth: Auth{ + SuperUsername: "postgres", + }, + ConnectionPooler: ConnectionPooler{ + NumberOfInstances: int32Ptr(2), + User: "pooler", + }, + Workers: 4, + }, + expectError: true, + errorMsg: "minimum number of instances", + }, + { + description: "workers set to zero", + cfg: Config{ + Resources: Resources{ + MinInstances: 1, + MaxInstances: 5, + }, + Auth: Auth{ + SuperUsername: "postgres", + }, + ConnectionPooler: ConnectionPooler{ + NumberOfInstances: int32Ptr(2), + User: "pooler", + }, + Workers: 0, + }, + expectError: true, + errorMsg: "number of workers should be higher than 0", + }, + { + description: "connection pooler instances below minimum", + cfg: Config{ + Resources: Resources{ + MinInstances: 1, + MaxInstances: 5, + }, + Auth: Auth{ + SuperUsername: "postgres", + }, + ConnectionPooler: ConnectionPooler{ + NumberOfInstances: int32Ptr(0), + User: "pooler", + }, + Workers: 4, + }, + expectError: true, + errorMsg: "number of connection pooler instances", + }, + { + description: "connection pooler user same as super user", + cfg: Config{ + Resources: Resources{ + MinInstances: 1, + MaxInstances: 5, + }, + Auth: Auth{ + SuperUsername: "postgres", + }, + ConnectionPooler: ConnectionPooler{ + NumberOfInstances: int32Ptr(2), + User: "postgres", + }, + Workers: 4, + }, + expectError: true, + errorMsg: "connection pool user is not allowed to be the same as super user", + }, + { + description: "min and max instances both negative (disabled)", + cfg: Config{ + Resources: Resources{ + MinInstances: -1, + MaxInstances: -1, + }, + Auth: Auth{ + SuperUsername: "postgres", + }, + ConnectionPooler: ConnectionPooler{ + NumberOfInstances: int32Ptr(2), + User: "pooler", + }, + Workers: 4, + }, + expectError: false, + }, +} + +func TestValidate(t *testing.T) { + for _, tt := range validateTests { + t.Run(tt.description, func(t *testing.T) { + err := validate(&tt.cfg) + if tt.expectError { + if err == nil { + t.Errorf("expected error containing %q, got nil", tt.errorMsg) + return + } + if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("expected error containing %q, got %q", tt.errorMsg, err.Error()) + } + } else { + if err != nil { + t.Errorf("expected no error, got %v", err) + } + } + }) + } +} + +var newFromMapTests = []struct { + description string + input map[string]string + expectPanic bool + panicMsg string + validateFunc func(t *testing.T, cfg *Config) +}{ + { + description: "empty map uses defaults", + input: map[string]string{}, + expectPanic: false, + validateFunc: func(t *testing.T, cfg *Config) { + if cfg.Workers != 8 { + t.Errorf("expected default Workers=8, got %d", cfg.Workers) + } + if cfg.SuperUsername != "postgres" { + t.Errorf("expected default SuperUsername=postgres, got %s", cfg.SuperUsername) + } + if cfg.ReplicationUsername != "standby" { + t.Errorf("expected default ReplicationUsername=standby, got %s", cfg.ReplicationUsername) + } + }, + }, + { + description: "custom values override defaults", + input: map[string]string{ + "workers": "16", + "super_username": "admin", + }, + expectPanic: false, + validateFunc: func(t *testing.T, cfg *Config) { + if cfg.Workers != 16 { + t.Errorf("expected Workers=16, got %d", cfg.Workers) + } + if cfg.SuperUsername != "admin" { + t.Errorf("expected SuperUsername=admin, got %s", cfg.SuperUsername) + } + }, + }, + { + description: "duration parsing", + input: map[string]string{ + "ready_wait_interval": "10s", + "ready_wait_timeout": "1m", + }, + expectPanic: false, + validateFunc: func(t *testing.T, cfg *Config) { + if cfg.ReadyWaitInterval.Seconds() != 10 { + t.Errorf("expected ReadyWaitInterval=10s, got %v", cfg.ReadyWaitInterval) + } + if cfg.ReadyWaitTimeout.Minutes() != 1 { + t.Errorf("expected ReadyWaitTimeout=1m, got %v", cfg.ReadyWaitTimeout) + } + }, + }, + { + description: "boolean parsing", + input: map[string]string{ + "enable_teams_api": "false", + "debug_logging": "false", + }, + expectPanic: false, + validateFunc: func(t *testing.T, cfg *Config) { + if cfg.EnableTeamsAPI != false { + t.Errorf("expected EnableTeamsAPI=false, got %v", cfg.EnableTeamsAPI) + } + if cfg.DebugLogging != false { + t.Errorf("expected DebugLogging=false, got %v", cfg.DebugLogging) + } + }, + }, + { + description: "map parsing", + input: map[string]string{ + "cluster_labels": "app:myapp,env:prod", + }, + expectPanic: false, + validateFunc: func(t *testing.T, cfg *Config) { + if cfg.ClusterLabels["app"] != "myapp" { + t.Errorf("expected ClusterLabels[app]=myapp, got %s", cfg.ClusterLabels["app"]) + } + if cfg.ClusterLabels["env"] != "prod" { + t.Errorf("expected ClusterLabels[env]=prod, got %s", cfg.ClusterLabels["env"]) + } + }, + }, + { + description: "slice parsing", + input: map[string]string{ + "inherited_labels": "label1,label2,label3", + }, + expectPanic: false, + validateFunc: func(t *testing.T, cfg *Config) { + expected := []string{"label1", "label2", "label3"} + if !reflect.DeepEqual(cfg.InheritedLabels, expected) { + t.Errorf("expected InheritedLabels=%v, got %v", expected, cfg.InheritedLabels) + } + }, + }, + { + description: "invalid workers triggers validation panic", + input: map[string]string{ + "workers": "0", + }, + expectPanic: true, + panicMsg: "number of workers should be higher than 0", + }, + { + description: "invalid integer causes panic", + input: map[string]string{ + "workers": "invalid", + }, + expectPanic: true, + panicMsg: "invalid syntax", + }, +} + +func TestNewFromMap(t *testing.T) { + for _, tt := range newFromMapTests { + t.Run(tt.description, func(t *testing.T) { + if tt.expectPanic { + defer func() { + r := recover() + if r == nil { + t.Errorf("expected panic with message containing %q, but no panic occurred", tt.panicMsg) + return + } + errMsg := fmt.Sprintf("%v", r) + if !strings.Contains(errMsg, tt.panicMsg) { + t.Errorf("expected panic message containing %q, got %q", tt.panicMsg, errMsg) + } + }() + } + + cfg := NewFromMap(tt.input) + + if tt.expectPanic { + t.Errorf("expected panic but NewFromMap returned successfully") + return + } + + if tt.validateFunc != nil { + tt.validateFunc(t, cfg) + } + }) + } +}