diff --git a/internal/config/config.go b/internal/config/config.go index 5a5cfe2..4fbe16e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "net/url" "os" "time" @@ -46,10 +47,10 @@ type ServerConfig struct { } type CacheConfig struct { - DBPath string `yaml:"db_path"` - MaxEntries int `yaml:"max_entries"` + DBPath string `yaml:"db_path"` + MaxEntries int `yaml:"max_entries"` TTL Duration `yaml:"ttl"` - LatencyAlpha float64 `yaml:"latency_alpha"` + LatencyAlpha float64 `yaml:"latency_alpha"` } type MeshConfig struct { @@ -100,6 +101,37 @@ func defaults() Config { } } +// Validates config fields. Call after Load. +func (c *Config) Validate() error { + if len(c.Upstreams) == 0 { + return fmt.Errorf("at least one upstream is required") + } + for i, u := range c.Upstreams { + if u.URL == "" { + return fmt.Errorf("upstream[%d]: URL is empty", i) + } + if _, err := url.ParseRequestURI(u.URL); err != nil { + return fmt.Errorf("upstream[%d]: invalid URL %q: %w", i, u.URL, err) + } + } + if c.Server.Listen == "" { + return fmt.Errorf("server.listen is empty") + } + if c.Cache.LatencyAlpha <= 0 || c.Cache.LatencyAlpha >= 1 { + return fmt.Errorf("cache.latency_alpha must be between 0 and 1 exclusive, got %f", c.Cache.LatencyAlpha) + } + if c.Cache.TTL.Duration <= 0 { + return fmt.Errorf("cache.ttl must be positive") + } + if c.Cache.MaxEntries <= 0 { + return fmt.Errorf("cache.max_entries must be positive") + } + if c.Mesh.Enabled && len(c.Mesh.Peers) == 0 { + return fmt.Errorf("mesh.enabled is true but no peers configured") + } + return nil +} + // Loads config from file (if non-empty) and applies env overrides. func Load(path string) (*Config, error) { cfg := defaults() diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 335f649..b1d2994 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -97,6 +97,58 @@ mesh: } } +func TestValidateValid(t *testing.T) { + cfg, _ := config.Load("") + if err := cfg.Validate(); err != nil { + t.Errorf("default config should be valid: %v", err) + } +} + +func TestValidateNoUpstreams(t *testing.T) { + cfg, _ := config.Load("") + cfg.Upstreams = nil + if err := cfg.Validate(); err == nil { + t.Error("expected error for no upstreams") + } +} + +func TestValidateBadURL(t *testing.T) { + cfg, _ := config.Load("") + cfg.Upstreams = []config.UpstreamConfig{{URL: "not-a-url"}} + if err := cfg.Validate(); err == nil { + t.Error("expected error for invalid URL") + } +} + +func TestValidateBadAlpha(t *testing.T) { + cfg, _ := config.Load("") + cfg.Cache.LatencyAlpha = 0 + if err := cfg.Validate(); err == nil { + t.Error("expected error for alpha=0") + } + cfg.Cache.LatencyAlpha = 1 + if err := cfg.Validate(); err == nil { + t.Error("expected error for alpha=1") + } +} + +func TestValidateNegativeTTL(t *testing.T) { + cfg, _ := config.Load("") + cfg.Cache.TTL = config.Duration{} + if err := cfg.Validate(); err == nil { + t.Error("expected error for zero TTL") + } +} + +func TestValidateMeshEnabledNoPeers(t *testing.T) { + cfg, _ := config.Load("") + cfg.Mesh.Enabled = true + cfg.Mesh.Peers = nil + if err := cfg.Validate(); err == nil { + t.Error("expected error for mesh enabled without peers") + } +} + func TestInvalidDuration(t *testing.T) { yamlContent := ` server: