package config import ( "os" "path/filepath" "testing" ) func TestLoadValid(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [[listeners]] addr = ":443" [[listeners.routes]] hostname = "example.com" backend = "127.0.0.1:8443" [proxy] connect_timeout = "5s" idle_timeout = "300s" shutdown_timeout = "30s" [log] level = "info" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } cfg, err := Load(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.Listeners) != 1 { t.Fatalf("got %d listeners, want 1", len(cfg.Listeners)) } if cfg.Listeners[0].Addr != ":443" { t.Fatalf("got listener addr %q, want %q", cfg.Listeners[0].Addr, ":443") } if len(cfg.Listeners[0].Routes) != 1 { t.Fatalf("got %d routes, want 1", len(cfg.Listeners[0].Routes)) } if cfg.Listeners[0].Routes[0].Hostname != "example.com" { t.Fatalf("got hostname %q, want %q", cfg.Listeners[0].Routes[0].Hostname, "example.com") } } func TestLoadNoDatabasePath(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [[listeners]] addr = ":443" [[listeners.routes]] hostname = "example.com" backend = "127.0.0.1:8443" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } _, err := Load(path) if err == nil { t.Fatal("expected error for missing database path") } } func TestLoadNoListenersValid(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") // No listeners is valid — DB may already have them. data := ` [database] path = "/tmp/test.db" [log] level = "info" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } _, err := Load(path) if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestLoadDuplicateHostnames(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [[listeners]] addr = ":443" [[listeners.routes]] hostname = "example.com" backend = "127.0.0.1:8443" [[listeners.routes]] hostname = "example.com" backend = "127.0.0.1:9443" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } _, err := Load(path) if err == nil { t.Fatal("expected error for duplicate hostnames") } } func TestLoadGeoIPRequiredWithCountries(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [[listeners]] addr = ":443" [[listeners.routes]] hostname = "example.com" backend = "127.0.0.1:8443" [firewall] blocked_countries = ["CN"] ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } _, err := Load(path) if err == nil { t.Fatal("expected error for blocked_countries without geoip_db") } } func TestLoadMultipleListeners(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [[listeners]] addr = ":443" [[listeners.routes]] hostname = "public.example.com" backend = "127.0.0.1:8443" [[listeners]] addr = ":8443" [[listeners.routes]] hostname = "internal.example.com" backend = "127.0.0.1:9443" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } cfg, err := Load(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.Listeners) != 2 { t.Fatalf("got %d listeners, want 2", len(cfg.Listeners)) } if cfg.Listeners[0].Routes[0].Hostname != "public.example.com" { t.Fatalf("listener 0 hostname = %q, want %q", cfg.Listeners[0].Routes[0].Hostname, "public.example.com") } if cfg.Listeners[1].Routes[0].Hostname != "internal.example.com" { t.Fatalf("listener 1 hostname = %q, want %q", cfg.Listeners[1].Routes[0].Hostname, "internal.example.com") } } func TestGRPCSocketPath(t *testing.T) { tests := []struct { addr string want string }{ {"/var/run/mc-proxy.sock", "/var/run/mc-proxy.sock"}, {"unix:/var/run/mc-proxy.sock", "/var/run/mc-proxy.sock"}, } for _, tt := range tests { g := GRPC{Addr: tt.addr} if got := g.SocketPath(); got != tt.want { t.Fatalf("SocketPath(%q) = %q, want %q", tt.addr, got, tt.want) } } } func TestValidateGRPCUnixSocket(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [grpc] addr = "/var/run/mc-proxy.sock" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } _, err := Load(path) if err != nil { t.Fatalf("expected Unix socket to be valid, got: %v", err) } } func TestValidateGRPCRejectsTCPAddr(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [grpc] addr = "127.0.0.1:9090" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } _, err := Load(path) if err == nil { t.Fatal("expected error for TCP gRPC addr") } } func TestValidateRateLimitRequiresWindow(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [firewall] rate_limit = 100 ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } _, err := Load(path) if err == nil { t.Fatal("expected error for rate_limit without rate_window") } } func TestValidateRateLimitWithWindow(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [firewall] rate_limit = 100 rate_window = "1m" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } cfg, err := Load(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if cfg.Firewall.RateLimit != 100 { t.Fatalf("got rate_limit %d, want 100", cfg.Firewall.RateLimit) } } func TestDuration(t *testing.T) { var d Duration if err := d.UnmarshalText([]byte("5s")); err != nil { t.Fatalf("unexpected error: %v", err) } if d.Duration.Seconds() != 5 { t.Fatalf("got %v, want 5s", d.Duration) } } func TestEnvOverrides(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [proxy] idle_timeout = "60s" [log] level = "info" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } // Set env overrides. t.Setenv("MCPROXY_LOG_LEVEL", "debug") t.Setenv("MCPROXY_PROXY_IDLE_TIMEOUT", "600s") t.Setenv("MCPROXY_DATABASE_PATH", "/override/test.db") cfg, err := Load(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if cfg.Log.Level != "debug" { t.Fatalf("got log.level %q, want %q", cfg.Log.Level, "debug") } if cfg.Proxy.IdleTimeout.Duration.Seconds() != 600 { t.Fatalf("got idle_timeout %v, want 600s", cfg.Proxy.IdleTimeout.Duration) } if cfg.Database.Path != "/override/test.db" { t.Fatalf("got database.path %q, want %q", cfg.Database.Path, "/override/test.db") } } func TestEnvOverrideInvalidDuration(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } t.Setenv("MCPROXY_PROXY_IDLE_TIMEOUT", "not-a-duration") // Invalid duration env overrides are silently ignored by the // mcdsl reflection-based loader. The config loads successfully // with the zero value for the field. cfg, err := Load(path) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Proxy.IdleTimeout.Duration != 0 { t.Fatalf("idle_timeout = %v, want 0 (invalid env ignored)", cfg.Proxy.IdleTimeout.Duration) } } func TestEnvOverrideGRPCAddr(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } t.Setenv("MCPROXY_GRPC_ADDR", "/var/run/override.sock") cfg, err := Load(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if cfg.GRPC.Addr != "/var/run/override.sock" { t.Fatalf("got grpc.addr %q, want %q", cfg.GRPC.Addr, "/var/run/override.sock") } } func TestLoadL4ModeDefault(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [[listeners]] addr = ":443" [[listeners.routes]] hostname = "example.com" backend = "127.0.0.1:8443" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } cfg, err := Load(path) if err != nil { t.Fatalf("unexpected error: %v", err) } // Mode should be normalized to "l4" when unset. if cfg.Listeners[0].Routes[0].Mode != "l4" { t.Fatalf("got mode %q, want %q", cfg.Listeners[0].Routes[0].Mode, "l4") } } func TestLoadInvalidMode(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [[listeners]] addr = ":443" [[listeners.routes]] hostname = "example.com" backend = "127.0.0.1:8443" mode = "l5" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } _, err := Load(path) if err == nil { t.Fatal("expected error for invalid mode") } } func TestLoadL7RequiresCertKey(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [[listeners]] addr = ":443" [[listeners.routes]] hostname = "example.com" backend = "127.0.0.1:8080" mode = "l7" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } _, err := Load(path) if err == nil { t.Fatal("expected error for L7 route without cert/key") } } func TestLoadL7InvalidCertKey(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [[listeners]] addr = ":443" [[listeners.routes]] hostname = "example.com" backend = "127.0.0.1:8080" mode = "l7" tls_cert = "/nonexistent/cert.pem" tls_key = "/nonexistent/key.pem" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } _, err := Load(path) if err == nil { t.Fatal("expected error for L7 route with nonexistent cert/key files") } } func TestLoadProxyProtocol(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [[listeners]] addr = ":443" proxy_protocol = true [[listeners.routes]] hostname = "example.com" backend = "127.0.0.1:8443" send_proxy_protocol = true ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } cfg, err := Load(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if !cfg.Listeners[0].ProxyProtocol { t.Fatal("expected proxy_protocol = true") } if !cfg.Listeners[0].Routes[0].SendProxyProtocol { t.Fatal("expected send_proxy_protocol = true") } } func TestLoadMetricsConfig(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [metrics] addr = "127.0.0.1:9090" path = "/metrics" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } cfg, err := Load(path) if err != nil { t.Fatalf("unexpected error: %v", err) } if cfg.Metrics.Addr != "127.0.0.1:9090" { t.Fatalf("got metrics.addr %q, want %q", cfg.Metrics.Addr, "127.0.0.1:9090") } if cfg.Metrics.Path != "/metrics" { t.Fatalf("got metrics.path %q, want %q", cfg.Metrics.Path, "/metrics") } } func TestValidateMetricsInvalidPath(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.toml") data := ` [database] path = "/tmp/test.db" [metrics] addr = "127.0.0.1:9090" path = "no-slash" ` if err := os.WriteFile(path, []byte(data), 0600); err != nil { t.Fatalf("write config: %v", err) } _, err := Load(path) if err == nil { t.Fatal("expected error for metrics.path without leading slash") } }