package config import ( "fmt" "os" "path/filepath" "strconv" "strings" "testing" "time" "go.yaml.in/yaml/v4" "proxy-pool/internal/domain/authorization" "proxy-pool/internal/domain/clientpolicy" ) const validConfig = ` version: 1 security: requireProtectionOnPublicListen: true gateway: enabled: true listen: 127.0.0.1:8080 auth: mode: none distribution: enabled: true listen: 127.0.0.1:8081 auth: mode: none extraction: fulfillment: partial maxCountPerRequest: 20 minRemainingTTL: 30s maxHealthCheckAge: 15s reserveForGateway: 5 routing: - name: extract enabled: true purpose: extract upstreams: [provider-a, provider-b] strategy: type: sequential switchAfterEmptyFetch: 5 endBehavior: stayLast onUnavailable: action: reject upstreams: provider-a: &valid-upstream enabled: true exposure: [gateway, extract] provider: billingMode: fetch protocols: [http] api: url: https://provider.example/proxies method: GET template: '{{.}}' auth: type: none proxyAuth: type: response pool: maxSize: 100 capacity: maxConcurrencyPerProxy: 10 refill: reconcileInterval: 1s minimumAvailableSlots: 200 targetAvailableSlots: 500 lifecycle: ttl: 120s allocationSafetyMargin: 10s fetch: estimatedIPsPerCall: 20 requestInterval: 1s timeout: 3s maxAttempts: 5 maxInFlight: 1 maxTotal: 1000 check: interval: 30s jitter: 20 maxInFlight: 100 timeout: 2s maxAttempts: 2 maxConsecutiveFailures: 3 urls: [http://connect.rom.miui.com/generate_204] provider-b: *valid-upstream ` func TestLoadStrictValidConfiguration(t *testing.T) { cfg, err := Load(strings.NewReader(validConfig)) if err != nil { t.Fatalf("Load(): %v", err) } provider := cfg.Upstreams["provider-a"] if cfg.Version != 1 || provider.Pool.MaxSize != 100 || provider.Fetch.EstimatedIPsPerCall != 20 || provider.Refill.ReconcileInterval.Value() != time.Second || provider.Refill.MinimumAvailableSlots != 200 || provider.Refill.TargetAvailableSlots != 500 { t.Fatalf("unexpected config: %+v", cfg) } } func TestLoadPreservesOmittedAndExplicitDestinationPolicyBooleans(t *testing.T) { source := strings.Replace(validConfig, " auth:\n mode: none\n", " auth:\n mode: none\n destinationPolicy:\n denyLoopback: false\n", 1, ) cfg, err := Load(strings.NewReader(source)) if err != nil { t.Fatalf("Load() error = %v", err) } policy := cfg.Gateway.DestinationPolicy if policy.DenyLoopback == nil || *policy.DenyLoopback { t.Fatalf("denyLoopback = %v, want explicit false", policy.DenyLoopback) } if policy.DenyPrivateNetworks != nil || policy.DenyLinkLocal != nil { t.Fatalf("omitted policy fields = (%v, %v), want nil", policy.DenyPrivateNetworks, policy.DenyLinkLocal) } redacted := cfg.Redacted() *redacted.Gateway.DestinationPolicy.DenyLoopback = true if *cfg.Gateway.DestinationPolicy.DenyLoopback { t.Fatal("Redacted() destination policy aliases source configuration") } } func TestLoadRejectsUnknownFields(t *testing.T) { _, err := Load(strings.NewReader(validConfig + "unknownField: true\n")) if err == nil || !strings.Contains(err.Error(), "unknownField") { t.Fatalf("Load() error = %v, want unknown field error", err) } } func TestValidateRejectsUnprotectedPublicListener(t *testing.T) { cfg, err := Load(strings.NewReader(strings.Replace(validConfig, "listen: 127.0.0.1:8080", "listen: 0.0.0.0:8080", 1))) if err == nil || !strings.Contains(err.Error(), "gateway") || !strings.Contains(err.Error(), "public") { t.Fatalf("Load() error = %v, want unprotected public listener error", err) } if cfg != nil { t.Fatal("invalid config must not be returned") } } func TestValidateRejectsMissingUpstreamReference(t *testing.T) { broken := strings.Replace(validConfig, "upstreams: [provider-a, provider-b]", "upstreams: [provider-a, missing]", 1) _, err := Load(strings.NewReader(broken)) if err == nil || !strings.Contains(err.Error(), "missing") { t.Fatalf("Load() error = %v, want missing upstream error", err) } } func TestValidateSeparatesPoolAndFetchLimits(t *testing.T) { broken := strings.Replace(validConfig, "maxTotal: 1000", "maxTotal: 50", 1) _, err := Load(strings.NewReader(broken)) if err == nil || !strings.Contains(err.Error(), "maxTotal") { t.Fatalf("Load() error = %v, want maxTotal validation error", err) } } func TestShippedConfigurationsAreValid(t *testing.T) { paths, err := filepath.Glob(filepath.Join("..", "..", "examples", "config", "*.yaml")) if err != nil { t.Fatalf("Glob(): %v", err) } paths = append(paths, filepath.Join("..", "..", "configs", "proxy-pool.yaml")) if len(paths) != 21 { t.Fatalf("configuration count = %d, want 21", len(paths)) } for _, path := range paths { path := path t.Run(filepath.Base(path), func(t *testing.T) { file, err := os.Open(path) if err != nil { t.Fatalf("Open(): %v", err) } defer file.Close() if _, err := Load(file); err != nil { t.Fatalf("Load(): %v", err) } }) } } func TestShippedDeploymentConfigurationsResolveEnvironment(t *testing.T) { resolver := fixtureResolver{environment: map[string]string{ "PROXY_POOL_GATEWAY_USERNAME": "resolved-gateway-user", "PROXY_POOL_GATEWAY_PASSWORD": "resolved-gateway-password", "PROXY_POOL_EXTRACT_TOKEN": "resolved-extract-token", "PROXY_POOL_ADMIN_TOKEN": "resolved-admin-token", "PROXY_POOL_POSTGRES_URL": "postgres://resolved", "PROXY_POOL_REDIS_URL": "redis://resolved", "PROVIDER_A_TOKEN": "resolved-provider-a-token", "PROVIDER_B_TOKEN": "resolved-provider-b-token", }} tests := []struct { name string path string configMap bool wantGatewayUser string wantPostgresURL string wantRedisURL string }{ { name: "local", path: filepath.Join("..", "..", "deploy", "config", "local.yaml"), wantGatewayUser: "local-gateway", wantPostgresURL: "postgres://proxy_pool:local-only-change-me@postgres:5432/proxy_pool?sslmode=disable", wantRedisURL: "redis://redis:6379/0", }, { name: "kubernetes", path: filepath.Join("..", "..", "deploy", "kubernetes", "base", "configmap.yaml"), configMap: true, wantGatewayUser: "resolved-gateway-user", wantPostgresURL: "postgres://resolved", wantRedisURL: "redis://resolved", }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { content, err := os.ReadFile(test.path) if err != nil { t.Fatalf("ReadFile(): %v", err) } configuration := string(content) if test.configMap { var manifest struct { Data map[string]string `yaml:"data"` } if err := yaml.Unmarshal(content, &manifest); err != nil { t.Fatalf("Unmarshal(): %v", err) } configuration = manifest.Data["config.yaml"] } cfg, err := LoadResolved(strings.NewReader(configuration), resolver) if err != nil { t.Fatalf("LoadResolved(): %v", err) } if cfg.Gateway.Auth.Username != test.wantGatewayUser || cfg.Gateway.Auth.Password != "resolved-gateway-password" || cfg.Distribution.Auth.Token != "resolved-extract-token" || cfg.Admin.Auth.Token != "resolved-admin-token" || cfg.Storage.PostgresURL != test.wantPostgresURL || cfg.Storage.RedisURL != test.wantRedisURL || cfg.Upstreams["provider-a"].API.Auth.Value != "resolved-provider-a-token" || cfg.Upstreams["provider-b"].API.Auth.Value != "resolved-provider-b-token" { t.Fatalf("deployment values were not resolved: %+v", cfg.Redacted()) } }) } } func TestLoadResolvedExpandsEnvironmentWithoutChangingTemplateVariables(t *testing.T) { configured := strings.Replace(validConfig, ` auth: type: none`, ` auth: type: basic username: "${PROVIDER_USER}" password: "${PROVIDER_PASSWORD}"`, 1) configured = strings.Replace(configured, "template: '{{.}}'", "template: '{{$x := .}}{{$x}}'", 1) resolver := fixtureResolver{environment: map[string]string{ "PROVIDER_USER": "alice", "PROVIDER_PASSWORD": "secret", }} cfg, err := LoadResolved(strings.NewReader(configured), resolver) if err != nil { t.Fatalf("LoadResolved(): %v", err) } auth := cfg.Upstreams["provider-a"].API.Auth if auth.Username != "alice" || auth.Password != "secret" { t.Fatalf("resolved auth = %+v", auth) } if got := cfg.Upstreams["provider-a"].API.Template; got != "{{$x := .}}{{$x}}" { t.Fatalf("template = %q, want template variable unchanged", got) } } func TestLoadResolvedReadsSecretFileAndClearsReference(t *testing.T) { configured := strings.Replace(validConfig, ` auth: type: none`, ` auth: type: basic username: alice passwordFile: /run/secrets/provider-password`, 1) resolver := fixtureResolver{files: map[string]string{ "/run/secrets/provider-password": "file-secret\r\n", }} cfg, err := LoadResolved(strings.NewReader(configured), resolver) if err != nil { t.Fatalf("LoadResolved(): %v", err) } auth := cfg.Upstreams["provider-a"].API.Auth if auth.Password != "file-secret" || auth.PasswordFile != "" { t.Fatalf("resolved auth = %+v", auth) } } func TestLoadRejectsMultipleYAMLDocuments(t *testing.T) { _, err := Load(strings.NewReader(validConfig + "\n---\nversion: 1\n")) if err == nil || !strings.Contains(err.Error(), "multiple YAML documents") { t.Fatalf("Load() error = %v, want multiple document error", err) } } func TestValidateRejectsUnsupportedListenerAuthMode(t *testing.T) { broken := strings.Replace(validConfig, "mode: none", "mode: custom", 1) _, err := Load(strings.NewReader(broken)) if err == nil || !strings.Contains(err.Error(), "auth.mode") { t.Fatalf("Load() error = %v, want auth.mode error", err) } } func TestValidateAcceptsBearerListenerAuthentication(t *testing.T) { cfg := mustLoadValidConfig(t) cfg.Distribution.Auth = Auth{Mode: "bearer", Token: "resolved-token"} cfg.Distribution.ClientIdentification.Mode = "authenticatedClient" if err := Validate(cfg); err != nil { t.Fatalf("Validate(bearer) error = %v", err) } cfg.Distribution.Auth = Auth{Mode: "any", Methods: []AuthMethod{{Mode: "bearer", Value: "method-token"}}} if err := Validate(cfg); err != nil { t.Fatalf("Validate(any bearer) error = %v", err) } } func TestValidateListenerPermissionConfiguration(t *testing.T) { t.Parallel() cfg := mustLoadValidConfig(t) cfg.Admin.Enabled = true cfg.Admin.Listen = "127.0.0.1:8082" cfg.Admin.Auth = Auth{ Mode: "bearer", Token: "admin-token", Permissions: []string{authorization.AdminRead}, } if err := Validate(cfg); err != nil { t.Fatalf("Validate(admin read permission) error = %v", err) } cfg.Admin.Auth.Permissions = []string{"unknown"} if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "permissions") { t.Fatalf("Validate(unknown permission) error = %v", err) } cfg.Admin.Auth = Auth{ Mode: "any", Permissions: []string{authorization.AdminRead}, Methods: []AuthMethod{{ Mode: "bearer", Value: "read-token", Permissions: []string{authorization.AdminRead}, }}, } if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "auth.permissions") { t.Fatalf("Validate(any top-level permission) error = %v", err) } cfg.Admin.Auth.Permissions = nil if err := Validate(cfg); err != nil { t.Fatalf("Validate(any method permission) error = %v", err) } } func TestValidateDistributionCredentialClientPolicy(t *testing.T) { t.Parallel() cfg, err := Load(strings.NewReader(validConfig)) if err != nil { t.Fatalf("Load() error = %v", err) } cfg.Distribution.Auth = Auth{ Mode: "bearer", Token: "distribution-token", ClientPolicy: clientpolicy.Policy{ MaxExtractCount: 5, AllowedUpstreams: []string{"provider-a"}, AllowedRegions: []string{"shanghai"}, }, } if err := Validate(cfg); err != nil { t.Fatalf("Validate(distribution client policy) error = %v", err) } cfg.Distribution.Auth.ClientPolicy.AllowedUpstreams = []string{"missing"} if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "missing") { t.Fatalf("Validate(unknown allowed upstream) error = %v", err) } cfg.Distribution.Auth.ClientPolicy = clientpolicy.Policy{} cfg.Gateway.Auth = Auth{ Mode: "bearer", Token: "gateway-token", ClientPolicy: clientpolicy.Policy{AllowedRoutings: []string{"extract"}}, } cfg.Routing[0].Purpose = "gateway" if err := Validate(cfg); err != nil { t.Fatalf("Validate(gateway routing policy) error = %v", err) } cfg.Gateway.Auth.ClientPolicy.AllowedRoutings = []string{"missing"} if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "missing") { t.Fatalf("Validate(unknown gateway routing) error = %v", err) } cfg.Gateway.Auth.ClientPolicy = clientpolicy.Policy{MaxExtractCount: 1} if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "only supported on distribution") { t.Fatalf("Validate(gateway extraction policy) error = %v", err) } } func TestValidateMetricsListener(t *testing.T) { t.Parallel() cfg := mustLoadValidConfig(t) cfg.Metrics = Metrics{Enabled: true} if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "metrics listen") { t.Fatalf("Validate(metrics without listen) error = %v", err) } cfg.Metrics.Listen = "not-an-address" if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "metrics listen") { t.Fatalf("Validate(invalid metrics listen) error = %v", err) } cfg.Metrics.Listen = "127.0.0.1:70000" if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "metrics listen") { t.Fatalf("Validate(out-of-range metrics port) error = %v", err) } cfg.Metrics.Listen = "0.0.0.0:9090" if err := Validate(cfg); err != nil { t.Fatalf("Validate(public metrics listener) error = %v", err) } cfg.Metrics = Metrics{Enabled: false, Listen: "not-an-address"} if err := Validate(cfg); err != nil { t.Fatalf("Validate(disabled metrics listener) error = %v", err) } } func TestValidateControlPlane(t *testing.T) { tests := []struct { name string mutate func(*Config) want string }{ { name: "missing listen", mutate: func(cfg *Config) { cfg.ControlPlane = validControlPlane() cfg.ControlPlane.Listen = "" }, want: "controlPlane listen", }, { name: "public plaintext", mutate: func(cfg *Config) { cfg.ControlPlane = validControlPlane() cfg.ControlPlane.Listen = "0.0.0.0:8443" }, want: "requires mtls", }, { name: "short session ttl", mutate: func(cfg *Config) { cfg.ControlPlane = validControlPlane() cfg.ControlPlane.SessionTTL = Duration(29 * time.Second) }, want: "sessionTTL", }, { name: "stale below heartbeat", mutate: func(cfg *Config) { cfg.ControlPlane = validControlPlane() cfg.ControlPlane.MaxStaleAge = Duration(9 * time.Second) }, want: "maxStaleAge", }, { name: "invalid protocol", mutate: func(cfg *Config) { cfg.ControlPlane = validControlPlane() cfg.ControlPlane.ProtocolVersion = 0 }, want: "protocolVersion", }, { name: "invalid message limit", mutate: func(cfg *Config) { cfg.ControlPlane = validControlPlane() cfg.ControlPlane.MaxMessageBytes = 64<<20 + 1 }, want: "maxMessageBytes", }, { name: "invalid counter limit", mutate: func(cfg *Config) { cfg.ControlPlane = validControlPlane() cfg.ControlPlane.MaxRuntimeCounters = MaximumPoolSize + 1 }, want: "maxRuntimeCounters", }, { name: "zero concurrent streams", mutate: func(cfg *Config) { cfg.ControlPlane = validControlPlane() cfg.ControlPlane.MaxConcurrentStreams = 0 }, want: "maxConcurrentStreams", }, { name: "unsupported tls mode", mutate: func(cfg *Config) { cfg.ControlPlane = validControlPlane() cfg.ControlPlane.TLS.Mode = "serverTLS" }, want: "tls.mode", }, { name: "missing mtls files", mutate: func(cfg *Config) { cfg.ControlPlane = validMTLSControlPlane() cfg.ControlPlane.TLS.CertFile = "" }, want: "certFile", }, { name: "trust domain with port", mutate: func(cfg *Config) { cfg.ControlPlane = validMTLSControlPlane() cfg.ControlPlane.TLS.TrustDomain = "proxy.example:443" }, want: "trustDomain", }, { name: "environment path", mutate: func(cfg *Config) { cfg.ControlPlane = validMTLSControlPlane() cfg.ControlPlane.TLS.Environment = "prod/eu" }, want: "environment", }, { name: "partial gateway tls", mutate: func(cfg *Config) { cfg.ControlPlane = validMTLSControlPlane() cfg.ControlPlane.GatewayTLS.CertFile = "/run/secrets/gateway-cert.pem" }, want: "gatewayTLS", }, { name: "partial checker tls", mutate: func(cfg *Config) { cfg.ControlPlane = validMTLSControlPlane() cfg.ControlPlane.CheckerTLS.KeyFile = "/run/secrets/checker-key.pem" }, want: "checkerTLS", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { cfg := mustLoadValidConfig(t) test.mutate(cfg) if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), test.want) { t.Fatalf("Validate() error = %v, want substring %q", err, test.want) } }) } } func validControlPlane() ControlPlane { return ControlPlane{ Enabled: true, Listen: "127.0.0.1:8443", ProtocolVersion: 1, HeartbeatInterval: Duration(10 * time.Second), SessionTTL: Duration(30 * time.Second), MaxStaleAge: Duration(10 * time.Second), MaxMessageBytes: 1 << 20, MaxRuntimeCounters: 100_000, MaxConcurrentStreams: 128, TLS: ControlPlaneTLS{Mode: "disabled"}, } } func validMTLSControlPlane() ControlPlane { controlPlane := validControlPlane() controlPlane.Listen = "0.0.0.0:8443" controlPlane.TLS = ControlPlaneTLS{ Mode: "mtls", CertFile: "/run/secrets/controller-cert.pem", KeyFile: "/run/secrets/controller-key.pem", ClientCAFile: "/run/secrets/worker-ca.pem", TrustDomain: "proxy.example", Environment: "production", } return controlPlane } func TestValidateRejectsInvalidConfigurationMatrix(t *testing.T) { tests := []struct { name string mutate func(*Config) want string }{ { name: "unsupported strategy type", mutate: func(cfg *Config) { cfg.Routing[0].Strategy.Type = "custom" }, want: "strategy.type", }, { name: "unsupported unavailable action", mutate: func(cfg *Config) { cfg.Routing[0].OnUnavailable.Action = "fallback" }, want: "onUnavailable.action", }, { name: "negative cumulative fetch limit", mutate: func(cfg *Config) { upstream := cfg.Upstreams["provider-a"] upstream.Fetch.MaxTotal = -1 cfg.Upstreams["provider-a"] = upstream }, want: "fetch.maxTotal", }, { name: "negative gateway reserve", mutate: func(cfg *Config) { cfg.Distribution.Extraction.ReserveForGateway = -1 }, want: "reserveForGateway", }, { name: "negative extraction idempotency ttl", mutate: func(cfg *Config) { cfg.Distribution.Extraction.IdempotencyTTL = Duration(-time.Second) }, want: "idempotencyTTL", }, { name: "enabled upstream without ttl", mutate: func(cfg *Config) { upstream := cfg.Upstreams["provider-a"] upstream.Lifecycle.TTL = 0 cfg.Upstreams["provider-a"] = upstream }, want: "lifecycle.ttl", }, { name: "no enabled upstream", mutate: func(cfg *Config) { for name, upstream := range cfg.Upstreams { upstream.Enabled = false cfg.Upstreams[name] = upstream } }, want: "enabled upstream", }, { name: "invalid trusted proxy CIDR", mutate: func(cfg *Config) { cfg.Gateway.Access.TrustedProxies = []string{"not-a-cidr"} }, want: "trustedProxies", }, { name: "negative listener request limit", mutate: func(cfg *Config) { cfg.Distribution.Limits.RequestsPerMinute = -1 }, want: "requestsPerMinute", }, { name: "listener request limit exceeds exact counter range", mutate: func(cfg *Config) { cfg.Distribution.Limits.RequestsPerMinutePerClient = int(MaximumExactCounter) + 1 }, want: "requestsPerMinutePerClient", }, { name: "invalid client identification mode", mutate: func(cfg *Config) { cfg.Distribution.ClientIdentification.Mode = "header" }, want: "clientIdentification.mode", }, { name: "authenticated client without authentication", mutate: func(cfg *Config) { cfg.Distribution.Auth = Auth{Mode: "none"} cfg.Distribution.ClientIdentification.Mode = "authenticatedClient" }, want: "authenticatedClient requires authentication", }, { name: "invalid destination deny CIDR", mutate: func(cfg *Config) { cfg.Gateway.DestinationPolicy.DenyCIDRs = []string{"not-a-cidr"} }, want: "denyCIDRs", }, { name: "invalid destination allowed port", mutate: func(cfg *Config) { cfg.Gateway.DestinationPolicy.AllowedPorts = []uint16{443, 0} }, want: "allowedPorts", }, { name: "duplicate destination allowed port", mutate: func(cfg *Config) { cfg.Gateway.DestinationPolicy.AllowedPorts = []uint16{443, 443} }, want: "allowedPorts", }, { name: "unsupported routing purpose", mutate: func(cfg *Config) { cfg.Routing[0].Purpose = "custom" }, want: "purpose", }, { name: "unsupported sequential end behavior", mutate: func(cfg *Config) { cfg.Routing[0].Strategy.EndBehavior = "restart" }, want: "endBehavior", }, { name: "sequential requires two upstreams", mutate: func(cfg *Config) { cfg.Routing[0].Upstreams = []string{"provider-a"} }, want: "at least two upstreams", }, { name: "weighted strategy missing weight", mutate: func(cfg *Config) { cfg.Routing[0].Strategy = Strategy{Type: "weighted", Weights: map[string]int{}} }, want: "weights", }, { name: "weighted strategy unknown upstream", mutate: func(cfg *Config) { cfg.Routing[0].Strategy = Strategy{Type: "weighted", Weights: map[string]int{ "provider-a": 1, "provider-b": 1, "provider-c": 1, }} }, want: "provider-c", }, { name: "weighted strategy nonpositive weight", mutate: func(cfg *Config) { cfg.Routing[0].Strategy = Strategy{Type: "weighted", Weights: map[string]int{"provider-a": 0}} }, want: "weight", }, { name: "negative fetch request interval", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Fetch.RequestInterval = Duration(-1) }) }, want: "requestInterval", }, { name: "zero fetch timeout", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Fetch.Timeout = 0 }) }, want: "fetch.timeout", }, { name: "zero fetch attempts", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Fetch.MaxAttempts = 0 }) }, want: "fetch.maxAttempts", }, { name: "zero fetch concurrency", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Fetch.MaxInFlight = 0 }) }, want: "fetch.maxInFlight", }, { name: "zero estimated IPs per call", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Fetch.EstimatedIPsPerCall = 0 }) }, want: "estimatedIPsPerCall", }, { name: "estimated IPs exceed pool size", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Fetch.EstimatedIPsPerCall = upstream.Pool.MaxSize + 1 }) }, want: "estimatedIPsPerCall", }, { name: "pool exceeds runtime scan bound", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Pool.MaxSize = MaximumPoolSize + 1 }) }, want: "pool.maxSize", }, { name: "zero refill interval", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Refill.ReconcileInterval = 0 }) }, want: "refill.reconcileInterval", }, { name: "refill target does not exceed minimum", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Refill.TargetAvailableSlots = upstream.Refill.MinimumAvailableSlots }) }, want: "targetAvailableSlots", }, { name: "refill target exceeds theoretical capacity", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Refill.TargetAvailableSlots = int64(upstream.Pool.MaxSize)*int64(upstream.Capacity.MaxConcurrencyPerProxy) + 1 }) }, want: "targetAvailableSlots", }, { name: "negative fetch response limit", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Fetch.MaxResponseBytes = -1 }) }, want: "maxResponseBytes", }, { name: "negative template timeout", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Fetch.TemplateTimeout = Duration(-1) }) }, want: "templateTimeout", }, { name: "fetch jitter over one hundred", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Fetch.Retry.Jitter = 101 }) }, want: "fetch.retry.jitter", }, { name: "zero check interval", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Check.Interval = 0 }) }, want: "check.interval", }, { name: "check jitter over one hundred", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Check.Jitter = 101 }) }, want: "check.jitter", }, { name: "zero check concurrency", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Check.MaxInFlight = 0 }) }, want: "check.maxInFlight", }, { name: "zero check timeout", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Check.Timeout = 0 }) }, want: "check.timeout", }, { name: "zero check attempts", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Check.MaxAttempts = 0 }) }, want: "check.maxAttempts", }, { name: "zero check failure threshold", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Check.MaxConsecutiveFailures = 0 }) }, want: "maxConsecutiveFailures", }, { name: "negative unhealthy remove after", mutate: func(cfg *Config) { updateUpstream(cfg, func(upstream *Upstream) { upstream.Check.UnhealthyRemoveAfter = Duration(-time.Second) }) }, want: "unhealthyRemoveAfter", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { cfg := mustLoadValidConfig(t) test.mutate(cfg) err := Validate(cfg) if err == nil || !strings.Contains(err.Error(), test.want) { t.Fatalf("Validate() error = %v, want substring %q", err, test.want) } }) } } func TestValidateRejectsUnboundedUpstreamCardinality(t *testing.T) { cfg := mustLoadValidConfig(t) for index := len(cfg.Upstreams); index <= MaximumUpstreams; index++ { cfg.Upstreams[fmt.Sprintf("disabled-%d", index)] = Upstream{} } if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "upstream count") { t.Fatalf("Validate(too many upstreams) error = %v", err) } } func TestValidateRejectsCountersOutsideRedisExactRange(t *testing.T) { if strconv.IntSize < 64 { t.Skip("64-bit int is required for values above the Redis exact range") } tests := []struct { name string mutate func(*Upstream) }{ {name: "fetch total", mutate: func(upstream *Upstream) { upstream.Fetch.MaxTotal = int(MaximumExactCounter) + 1 }}, {name: "proxy concurrency", mutate: func(upstream *Upstream) { upstream.Capacity.MaxConcurrencyPerProxy = int(MaximumExactCounter) + 1 }}, {name: "minimum slots", mutate: func(upstream *Upstream) { upstream.Refill.MinimumAvailableSlots = MaximumExactCounter + 1 upstream.Refill.TargetAvailableSlots = MaximumExactCounter + 2 }}, {name: "target slots", mutate: func(upstream *Upstream) { upstream.Capacity.MaxConcurrencyPerProxy = int(MaximumExactCounter) upstream.Pool.MaxSize = 1 upstream.Refill.MinimumAvailableSlots = MaximumExactCounter upstream.Refill.TargetAvailableSlots = MaximumExactCounter + 1 }}, {name: "theoretical slots", mutate: func(upstream *Upstream) { upstream.Pool.MaxSize = 2 upstream.Capacity.MaxConcurrencyPerProxy = int(MaximumExactCounter/2 + 1) upstream.Refill.MinimumAvailableSlots = 1 upstream.Refill.TargetAvailableSlots = 2 }}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { cfg := mustLoadValidConfig(t) updateUpstream(cfg, test.mutate) if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "exact counter range") { t.Fatalf("Validate(inexact counter) error = %v", err) } }) } } func TestResolvedConfigFormattingRedactsSecrets(t *testing.T) { configured := strings.Replace(validConfig, ` auth: mode: none`, ` auth: mode: usernamePassword username: gateway-user password: "${GATEWAY_PASSWORD}"`, 1) configured = strings.Replace(configured, ` auth: mode: none`, ` auth: mode: apiKey header: X-API-Key token: "${DISTRIBUTION_TOKEN}"`, 1) configured = strings.Replace(configured, ` auth: type: none`, ` auth: type: apiKey location: header name: X-Provider-Key value: "${PROVIDER_API_KEY}"`, 1) resolver := fixtureResolver{environment: map[string]string{ "GATEWAY_PASSWORD": "password-marker", "DISTRIBUTION_TOKEN": "token-marker", "PROVIDER_API_KEY": "api-key-marker", }} cfg, err := LoadResolved(strings.NewReader(configured), resolver) if err != nil { t.Fatalf("LoadResolved(): %v", err) } for _, format := range []string{"%v", "%+v", "%#v"} { formatted := fmt.Sprintf(format, cfg) for _, secret := range []string{"password-marker", "token-marker", "api-key-marker"} { if strings.Contains(formatted, secret) { t.Fatalf("format %q leaked secret %q: %s", format, secret, formatted) } } if !strings.Contains(formatted, "[REDACTED]") { t.Fatalf("format %q did not contain a redaction marker: %s", format, formatted) } } redacted := cfg.Redacted() if redacted.Gateway.Auth.Password != "[REDACTED]" || redacted.Distribution.Auth.Token != "[REDACTED]" || redacted.Upstreams["provider-a"].API.Auth.Value != "[REDACTED]" { t.Fatalf("Redacted() retained a secret: %+v", &redacted) } if cfg.Gateway.Auth.Password != "password-marker" { t.Fatal("Redacted() mutated the source configuration") } } func mustLoadValidConfig(t *testing.T) *Config { t.Helper() cfg, err := Load(strings.NewReader(validConfig)) if err != nil { t.Fatalf("Load(validConfig): %v", err) } return cfg } func updateUpstream(cfg *Config, update func(*Upstream)) { upstream := cfg.Upstreams["provider-a"] update(&upstream) cfg.Upstreams["provider-a"] = upstream } type fixtureResolver struct { environment map[string]string files map[string]string } func (r fixtureResolver) LookupEnv(name string) (string, bool) { value, ok := r.environment[name] return value, ok } func (r fixtureResolver) ReadFile(path string) ([]byte, error) { value, ok := r.files[path] if !ok { return nil, fmt.Errorf("fixture file %q not found", path) } return []byte(value), nil }