test: add unit tests for validate() and NewFromMap() functions
This addresses issue #416 by adding comprehensive unit tests for the config utility functions: - TestValidate: Tests validation logic for MinInstances/MaxInstances, Workers count, ConnectionPooler instances, and user conflicts - TestNewFromMap: Tests config creation from map with defaults, custom values, duration/boolean/map/slice parsing, and panic behavior - TestMain: Sets OPERATOR_NAMESPACE env var for testing outside K8s The tests cover both valid configurations and error cases.
This commit is contained in:
parent
0ac28e3aad
commit
db098bff96
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue