Files
ResolveSpec/pkg/config/manager_test.go
Hein 250fcf686c feat(config): add multiple server instances support
- Add ServersConfig and ServerInstanceConfig structs
- Support configuring multiple named server instances
- Add global timeout defaults with per-instance overrides
- Add TLS configuration options (SSL cert/key, self-signed, AutoTLS)
- Add validation for server configurations
- Add helper methods for applying defaults and getting default server
- Add conversion helper to avoid import cycles
2026-01-03 01:48:42 +02:00

609 lines
15 KiB
Go

package config
import (
"os"
"testing"
"time"
)
func TestNewManager(t *testing.T) {
mgr := NewManager()
if mgr == nil {
t.Fatal("Expected manager to be non-nil")
}
if mgr.v == nil {
t.Fatal("Expected viper instance to be non-nil")
}
}
func TestDefaultValues(t *testing.T) {
mgr := NewManager()
if err := mgr.Load(); err != nil {
t.Fatalf("Failed to load config: %v", err)
}
cfg, err := mgr.GetConfig()
if err != nil {
t.Fatalf("Failed to get config: %v", err)
}
// Test default values
tests := []struct {
name string
got interface{}
expected interface{}
}{
{"servers.default_server", cfg.Servers.DefaultServer, "default"},
{"servers.shutdown_timeout", cfg.Servers.ShutdownTimeout, 30 * time.Second},
{"tracing.enabled", cfg.Tracing.Enabled, false},
{"tracing.service_name", cfg.Tracing.ServiceName, "resolvespec"},
{"cache.provider", cfg.Cache.Provider, "memory"},
{"cache.redis.host", cfg.Cache.Redis.Host, "localhost"},
{"cache.redis.port", cfg.Cache.Redis.Port, 6379},
{"logger.dev", cfg.Logger.Dev, false},
{"middleware.rate_limit_rps", cfg.Middleware.RateLimitRPS, 100.0},
{"middleware.rate_limit_burst", cfg.Middleware.RateLimitBurst, 200},
}
// Test default server instance
defaultServer, ok := cfg.Servers.Instances["default"]
if !ok {
t.Fatal("Default server instance not found")
}
if defaultServer.Port != 8080 {
t.Errorf("default server port: got %d, want 8080", defaultServer.Port)
}
if defaultServer.Name != "default" {
t.Errorf("default server name: got %s, want default", defaultServer.Name)
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.expected {
t.Errorf("%s: got %v, want %v", tt.name, tt.got, tt.expected)
}
})
}
}
func TestEnvironmentVariableOverrides(t *testing.T) {
// Set environment variables
os.Setenv("RESOLVESPEC_SERVERS_INSTANCES_DEFAULT_PORT", "9090")
os.Setenv("RESOLVESPEC_TRACING_ENABLED", "true")
os.Setenv("RESOLVESPEC_CACHE_PROVIDER", "redis")
os.Setenv("RESOLVESPEC_LOGGER_DEV", "true")
defer func() {
os.Unsetenv("RESOLVESPEC_SERVERS_INSTANCES_DEFAULT_PORT")
os.Unsetenv("RESOLVESPEC_TRACING_ENABLED")
os.Unsetenv("RESOLVESPEC_CACHE_PROVIDER")
os.Unsetenv("RESOLVESPEC_LOGGER_DEV")
}()
mgr := NewManager()
if err := mgr.Load(); err != nil {
t.Fatalf("Failed to load config: %v", err)
}
cfg, err := mgr.GetConfig()
if err != nil {
t.Fatalf("Failed to get config: %v", err)
}
// Test environment variable overrides
tests := []struct {
name string
got interface{}
expected interface{}
}{
{"tracing.enabled", cfg.Tracing.Enabled, true},
{"cache.provider", cfg.Cache.Provider, "redis"},
{"logger.dev", cfg.Logger.Dev, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.expected {
t.Errorf("%s: got %v, want %v", tt.name, tt.got, tt.expected)
}
})
}
// Test server port override
defaultServer := cfg.Servers.Instances["default"]
if defaultServer.Port != 9090 {
t.Errorf("server port: got %d, want 9090", defaultServer.Port)
}
}
func TestProgrammaticConfiguration(t *testing.T) {
mgr := NewManager()
mgr.Set("servers.instances.default.port", 7070)
mgr.Set("tracing.service_name", "test-service")
cfg, err := mgr.GetConfig()
if err != nil {
t.Fatalf("Failed to get config: %v", err)
}
if cfg.Servers.Instances["default"].Port != 7070 {
t.Errorf("server port: got %d, want 7070", cfg.Servers.Instances["default"].Port)
}
if cfg.Tracing.ServiceName != "test-service" {
t.Errorf("tracing.service_name: got %s, want test-service", cfg.Tracing.ServiceName)
}
}
func TestGetterMethods(t *testing.T) {
mgr := NewManager()
mgr.Set("test.string", "value")
mgr.Set("test.int", 42)
mgr.Set("test.bool", true)
if got := mgr.GetString("test.string"); got != "value" {
t.Errorf("GetString: got %s, want value", got)
}
if got := mgr.GetInt("test.int"); got != 42 {
t.Errorf("GetInt: got %d, want 42", got)
}
if got := mgr.GetBool("test.bool"); !got {
t.Errorf("GetBool: got %v, want true", got)
}
}
func TestWithOptions(t *testing.T) {
mgr := NewManagerWithOptions(
WithEnvPrefix("MYAPP"),
WithConfigName("myconfig"),
)
if mgr == nil {
t.Fatal("Expected manager to be non-nil")
}
// Set environment variable with custom prefix
os.Setenv("MYAPP_SERVERS_INSTANCES_DEFAULT_PORT", "5000")
defer os.Unsetenv("MYAPP_SERVERS_INSTANCES_DEFAULT_PORT")
if err := mgr.Load(); err != nil {
t.Fatalf("Failed to load config: %v", err)
}
cfg, err := mgr.GetConfig()
if err != nil {
t.Fatalf("Failed to get config: %v", err)
}
if cfg.Servers.Instances["default"].Port != 5000 {
t.Errorf("server port: got %d, want 5000", cfg.Servers.Instances["default"].Port)
}
}
func TestServersConfig(t *testing.T) {
mgr := NewManager()
if err := mgr.Load(); err != nil {
t.Fatalf("Failed to load config: %v", err)
}
cfg, err := mgr.GetConfig()
if err != nil {
t.Fatalf("Failed to get config: %v", err)
}
// Test default server exists
if cfg.Servers.DefaultServer != "default" {
t.Errorf("Expected default_server to be 'default', got %s", cfg.Servers.DefaultServer)
}
// Test default instance
defaultServer, ok := cfg.Servers.Instances["default"]
if !ok {
t.Fatal("Default server instance not found")
}
if defaultServer.Port != 8080 {
t.Errorf("Expected default port 8080, got %d", defaultServer.Port)
}
if defaultServer.Name != "default" {
t.Errorf("Expected default name 'default', got %s", defaultServer.Name)
}
if defaultServer.Description != "Default HTTP server" {
t.Errorf("Expected description 'Default HTTP server', got %s", defaultServer.Description)
}
}
func TestMultipleServerInstances(t *testing.T) {
mgr := NewManager()
// Add additional server instances (default instance exists from defaults)
mgr.Set("servers.default_server", "api")
mgr.Set("servers.instances.api.name", "api")
mgr.Set("servers.instances.api.host", "0.0.0.0")
mgr.Set("servers.instances.api.port", 8080)
mgr.Set("servers.instances.admin.name", "admin")
mgr.Set("servers.instances.admin.host", "localhost")
mgr.Set("servers.instances.admin.port", 8081)
cfg, err := mgr.GetConfig()
if err != nil {
t.Fatalf("Failed to get config: %v", err)
}
// Should have default + api + admin = 3 instances
if len(cfg.Servers.Instances) < 2 {
t.Errorf("Expected at least 2 server instances, got %d", len(cfg.Servers.Instances))
}
// Verify api instance
apiServer, ok := cfg.Servers.Instances["api"]
if !ok {
t.Fatal("API server instance not found")
}
if apiServer.Port != 8080 {
t.Errorf("Expected API port 8080, got %d", apiServer.Port)
}
// Verify admin instance
adminServer, ok := cfg.Servers.Instances["admin"]
if !ok {
t.Fatal("Admin server instance not found")
}
if adminServer.Port != 8081 {
t.Errorf("Expected admin port 8081, got %d", adminServer.Port)
}
// Validate default server
if err := cfg.Servers.Validate(); err != nil {
t.Errorf("Server config validation failed: %v", err)
}
// Get default
defaultSrv, err := cfg.Servers.GetDefault()
if err != nil {
t.Fatalf("Failed to get default server: %v", err)
}
if defaultSrv.Name != "api" {
t.Errorf("Expected default server 'api', got '%s'", defaultSrv.Name)
}
}
func TestExtensionsField(t *testing.T) {
mgr := NewManager()
// Set custom extensions
mgr.Set("extensions.custom_feature.enabled", true)
mgr.Set("extensions.custom_feature.api_key", "test-key")
mgr.Set("extensions.another_extension.timeout", "5s")
cfg, err := mgr.GetConfig()
if err != nil {
t.Fatalf("Failed to get config: %v", err)
}
if cfg.Extensions == nil {
t.Fatal("Extensions should not be nil")
}
// Verify extensions are accessible
customFeature := mgr.Get("extensions.custom_feature")
if customFeature == nil {
t.Error("custom_feature extension not found")
}
// Verify via config manager methods
if !mgr.GetBool("extensions.custom_feature.enabled") {
t.Error("Expected custom_feature.enabled to be true")
}
if mgr.GetString("extensions.custom_feature.api_key") != "test-key" {
t.Error("Expected api_key to be 'test-key'")
}
}
func TestServerInstanceValidation(t *testing.T) {
tests := []struct {
name string
instance ServerInstanceConfig
expectErr bool
}{
{
name: "valid basic config",
instance: ServerInstanceConfig{
Name: "test",
Port: 8080,
},
expectErr: false,
},
{
name: "invalid port - too high",
instance: ServerInstanceConfig{
Name: "test",
Port: 99999,
},
expectErr: true,
},
{
name: "invalid port - zero",
instance: ServerInstanceConfig{
Name: "test",
Port: 0,
},
expectErr: true,
},
{
name: "empty name",
instance: ServerInstanceConfig{
Name: "",
Port: 8080,
},
expectErr: true,
},
{
name: "conflicting TLS options",
instance: ServerInstanceConfig{
Name: "test",
Port: 8080,
SelfSignedSSL: true,
AutoTLS: true,
},
expectErr: true,
},
{
name: "incomplete SSL cert config",
instance: ServerInstanceConfig{
Name: "test",
Port: 8080,
SSLCert: "/path/to/cert.pem",
// Missing SSLKey
},
expectErr: true,
},
{
name: "AutoTLS without domains",
instance: ServerInstanceConfig{
Name: "test",
Port: 8080,
AutoTLS: true,
// Missing AutoTLSDomains
},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.instance.Validate()
if tt.expectErr && err == nil {
t.Error("Expected validation error, got nil")
}
if !tt.expectErr && err != nil {
t.Errorf("Expected no error, got: %v", err)
}
})
}
}
func TestApplyGlobalDefaults(t *testing.T) {
globals := ServersConfig{
ShutdownTimeout: 30 * time.Second,
DrainTimeout: 25 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
instance := ServerInstanceConfig{
Name: "test",
Port: 8080,
}
// Apply global defaults
instance.ApplyGlobalDefaults(globals)
// Check that defaults were applied
if instance.ShutdownTimeout == nil || *instance.ShutdownTimeout != 30*time.Second {
t.Error("ShutdownTimeout not applied correctly")
}
if instance.DrainTimeout == nil || *instance.DrainTimeout != 25*time.Second {
t.Error("DrainTimeout not applied correctly")
}
if instance.ReadTimeout == nil || *instance.ReadTimeout != 10*time.Second {
t.Error("ReadTimeout not applied correctly")
}
if instance.WriteTimeout == nil || *instance.WriteTimeout != 10*time.Second {
t.Error("WriteTimeout not applied correctly")
}
if instance.IdleTimeout == nil || *instance.IdleTimeout != 120*time.Second {
t.Error("IdleTimeout not applied correctly")
}
// Test that explicit overrides are not replaced
customTimeout := 60 * time.Second
instance2 := ServerInstanceConfig{
Name: "test2",
Port: 8081,
ShutdownTimeout: &customTimeout,
}
instance2.ApplyGlobalDefaults(globals)
if instance2.ShutdownTimeout == nil || *instance2.ShutdownTimeout != 60*time.Second {
t.Error("Custom ShutdownTimeout was overridden")
}
}
func TestPathsConfig(t *testing.T) {
mgr := NewManager()
if err := mgr.Load(); err != nil {
t.Fatalf("Failed to load config: %v", err)
}
cfg, err := mgr.GetConfig()
if err != nil {
t.Fatalf("Failed to get config: %v", err)
}
// Test default paths exist
if !cfg.Paths.Has("data_dir") {
t.Error("Expected data_dir path to exist")
}
if !cfg.Paths.Has("config_dir") {
t.Error("Expected config_dir path to exist")
}
if !cfg.Paths.Has("logs_dir") {
t.Error("Expected logs_dir path to exist")
}
if !cfg.Paths.Has("temp_dir") {
t.Error("Expected temp_dir path to exist")
}
// Test Get method
dataDir, err := cfg.Paths.Get("data_dir")
if err != nil {
t.Errorf("Failed to get data_dir: %v", err)
}
if dataDir != "./data" {
t.Errorf("Expected data_dir to be './data', got '%s'", dataDir)
}
// Test GetOrDefault
existing := cfg.Paths.GetOrDefault("data_dir", "/default/path")
if existing != "./data" {
t.Errorf("Expected existing path, got '%s'", existing)
}
nonExisting := cfg.Paths.GetOrDefault("nonexistent", "/default/path")
if nonExisting != "/default/path" {
t.Errorf("Expected default path, got '%s'", nonExisting)
}
}
func TestPathsConfigMethods(t *testing.T) {
pc := PathsConfig{
"base": "/var/myapp",
"data": "/var/myapp/data",
}
// Test Get
path, err := pc.Get("base")
if err != nil {
t.Errorf("Failed to get path: %v", err)
}
if path != "/var/myapp" {
t.Errorf("Expected '/var/myapp', got '%s'", path)
}
// Test Get non-existent
_, err = pc.Get("nonexistent")
if err == nil {
t.Error("Expected error for non-existent path")
}
// Test Set
pc.Set("new_path", "/new/location")
newPath, err := pc.Get("new_path")
if err != nil {
t.Errorf("Failed to get newly set path: %v", err)
}
if newPath != "/new/location" {
t.Errorf("Expected '/new/location', got '%s'", newPath)
}
// Test Has
if !pc.Has("base") {
t.Error("Expected 'base' path to exist")
}
if pc.Has("nonexistent") {
t.Error("Expected 'nonexistent' path to not exist")
}
// Test List
names := pc.List()
if len(names) != 3 {
t.Errorf("Expected 3 paths, got %d", len(names))
}
// Test Join
joined, err := pc.Join("base", "subdir", "file.txt")
if err != nil {
t.Errorf("Failed to join paths: %v", err)
}
expected := "/var/myapp/subdir/file.txt"
if joined != expected {
t.Errorf("Expected '%s', got '%s'", expected, joined)
}
}
func TestPathsConfigEnvironmentVariables(t *testing.T) {
// Set environment variables for paths
os.Setenv("RESOLVESPEC_PATHS_DATA_DIR", "/custom/data")
os.Setenv("RESOLVESPEC_PATHS_LOGS_DIR", "/custom/logs")
defer func() {
os.Unsetenv("RESOLVESPEC_PATHS_DATA_DIR")
os.Unsetenv("RESOLVESPEC_PATHS_LOGS_DIR")
}()
mgr := NewManager()
if err := mgr.Load(); err != nil {
t.Fatalf("Failed to load config: %v", err)
}
cfg, err := mgr.GetConfig()
if err != nil {
t.Fatalf("Failed to get config: %v", err)
}
// Test environment variable override of existing default path
dataDir, err := cfg.Paths.Get("data_dir")
if err != nil {
t.Errorf("Failed to get data_dir: %v", err)
}
if dataDir != "/custom/data" {
t.Errorf("Expected '/custom/data', got '%s'", dataDir)
}
// Test another environment variable override
logsDir, err := cfg.Paths.Get("logs_dir")
if err != nil {
t.Errorf("Failed to get logs_dir: %v", err)
}
if logsDir != "/custom/logs" {
t.Errorf("Expected '/custom/logs', got '%s'", logsDir)
}
}
func TestPathsConfigProgrammatic(t *testing.T) {
mgr := NewManager()
// Set custom paths programmatically
mgr.Set("paths.custom_dir", "/my/custom/dir")
mgr.Set("paths.cache_dir", "/var/cache/myapp")
cfg, err := mgr.GetConfig()
if err != nil {
t.Fatalf("Failed to get config: %v", err)
}
// Verify custom paths
customDir, err := cfg.Paths.Get("custom_dir")
if err != nil {
t.Errorf("Failed to get custom_dir: %v", err)
}
if customDir != "/my/custom/dir" {
t.Errorf("Expected '/my/custom/dir', got '%s'", customDir)
}
cacheDir, err := cfg.Paths.Get("cache_dir")
if err != nil {
t.Errorf("Failed to get cache_dir: %v", err)
}
if cacheDir != "/var/cache/myapp" {
t.Errorf("Expected '/var/cache/myapp', got '%s'", cacheDir)
}
}