mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-01-10 13:14:24 +00:00
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
This commit is contained in:
@@ -34,8 +34,8 @@ func TestDefaultValues(t *testing.T) {
|
||||
got interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{"server.addr", cfg.Server.Addr, ":8080"},
|
||||
{"server.shutdown_timeout", cfg.Server.ShutdownTimeout, 30 * time.Second},
|
||||
{"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"},
|
||||
@@ -46,6 +46,18 @@ func TestDefaultValues(t *testing.T) {
|
||||
{"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 {
|
||||
@@ -57,12 +69,12 @@ func TestDefaultValues(t *testing.T) {
|
||||
|
||||
func TestEnvironmentVariableOverrides(t *testing.T) {
|
||||
// Set environment variables
|
||||
os.Setenv("RESOLVESPEC_SERVER_ADDR", ":9090")
|
||||
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_SERVER_ADDR")
|
||||
os.Unsetenv("RESOLVESPEC_SERVERS_INSTANCES_DEFAULT_PORT")
|
||||
os.Unsetenv("RESOLVESPEC_TRACING_ENABLED")
|
||||
os.Unsetenv("RESOLVESPEC_CACHE_PROVIDER")
|
||||
os.Unsetenv("RESOLVESPEC_LOGGER_DEV")
|
||||
@@ -84,7 +96,6 @@ func TestEnvironmentVariableOverrides(t *testing.T) {
|
||||
got interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{"server.addr", cfg.Server.Addr, ":9090"},
|
||||
{"tracing.enabled", cfg.Tracing.Enabled, true},
|
||||
{"cache.provider", cfg.Cache.Provider, "redis"},
|
||||
{"logger.dev", cfg.Logger.Dev, true},
|
||||
@@ -97,11 +108,17 @@ func TestEnvironmentVariableOverrides(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 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("server.addr", ":7070")
|
||||
mgr.Set("servers.instances.default.port", 7070)
|
||||
mgr.Set("tracing.service_name", "test-service")
|
||||
|
||||
cfg, err := mgr.GetConfig()
|
||||
@@ -109,8 +126,8 @@ func TestProgrammaticConfiguration(t *testing.T) {
|
||||
t.Fatalf("Failed to get config: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Server.Addr != ":7070" {
|
||||
t.Errorf("server.addr: got %s, want :7070", cfg.Server.Addr)
|
||||
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" {
|
||||
@@ -148,8 +165,8 @@ func TestWithOptions(t *testing.T) {
|
||||
}
|
||||
|
||||
// Set environment variable with custom prefix
|
||||
os.Setenv("MYAPP_SERVER_ADDR", ":5000")
|
||||
defer os.Unsetenv("MYAPP_SERVER_ADDR")
|
||||
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)
|
||||
@@ -160,7 +177,432 @@ func TestWithOptions(t *testing.T) {
|
||||
t.Fatalf("Failed to get config: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Server.Addr != ":5000" {
|
||||
t.Errorf("server.addr: got %s, want :5000", cfg.Server.Addr)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user