package config_test import ( "bytes" "crypto/x509" "encoding/json" "log/slog" "maps" "net/netip" "os" "path/filepath" "slices" "strings" "testing" "time" "sneak.berlin/go/smallwebwaf/internal/config" ) // The settings, by name. const ( listenAddr = "SWWAF_LISTEN_ADDR" upstreamURL = "SWWAF_UPSTREAM_URL" mode = "SWWAF_MODE" trustedProxies = "SWWAF_TRUSTED_PROXIES" clientRequestTimeout = "SWWAF_CLIENT_REQUEST_TIMEOUT" clientHeaderMaxBytes = "SWWAF_CLIENT_REQUEST_HEADER_MAX_BYTES" clientIdleTimeout = "SWWAF_CLIENT_IDLE_TIMEOUT" clientResponseTimeout = "SWWAF_CLIENT_RESPONSE_TIMEOUT" upstreamRequestTimeout = "SWWAF_UPSTREAM_REQUEST_TIMEOUT" upstreamResponseTimeout = "SWWAF_UPSTREAM_RESPONSE_TIMEOUT" requestMaxBytes = "SWWAF_REQUEST_MAX_BYTES" responseMaxBytes = "SWWAF_RESPONSE_MAX_BYTES" allowNets = "SWWAF_ALLOW_NETS" rateLimitExemptNets = "SWWAF_RATE_LIMIT_EXEMPT_NETS" denyNets = "SWWAF_DENY_NETS" rateLimitPerMinute = "SWWAF_RATE_LIMIT_PER_MINUTE" rateLimitPerHour = "SWWAF_RATE_LIMIT_PER_HOUR" rateLimitPerDay = "SWWAF_RATE_LIMIT_PER_DAY" deniedCountries = "SWWAF_DENIED_COUNTRIES" allowedCountries = "SWWAF_EXCLUSIVELY_ALLOWED_COUNTRIES" banResponse = "SWWAF_BAN_RESPONSE" limitBanDuration = "SWWAF_LIMIT_BAN_DURATION" limitBanRepeatWindow = "SWWAF_LIMIT_BAN_REPEAT_WINDOW" maxBanDuration = "SWWAF_MAX_BAN_DURATION" maxBans = "SWWAF_MAX_BANS" banScopeV4Prefix = "SWWAF_BAN_SCOPE_V4_PREFIX" stateDir = "SWWAF_STATE_DIR" stateWriteDelay = "SWWAF_STATE_WRITE_DELAY" stateCounterInterval = "SWWAF_STATE_COUNTER_INTERVAL" metricsToken = "SWWAF_METRICS_TOKEN" //nolint:gosec // the setting's name metricsTopN = "SWWAF_METRICS_TOP_N" logRemoteURL = "SWWAF_LOG_REMOTE_URL" logRemoteTLSCAFile = "SWWAF_LOG_REMOTE_TLS_CA_FILE" logRemoteBuffer = "SWWAF_LOG_REMOTE_BUFFER" logRemoteFacility = "SWWAF_LOG_REMOTE_FACILITY" logRemoteAppName = "SWWAF_LOG_REMOTE_APP_NAME" ) // testCA is a CA certificate, of which only that it reads matters here. const testCA = `-----BEGIN CERTIFICATE----- MIIBkzCCATmgAwIBAgIUeySaE27dnr6A2HijrMB13gTLUKIwCgYIKoZIzj0EAwIw HjEcMBoGA1UEAwwTc21hbGx3ZWJ3YWYgdGVzdCBDQTAgFw0yNjEwMDYxNDI2MTNa GA8yMTI2MDkxMjE0MjYxM1owHjEcMBoGA1UEAwwTc21hbGx3ZWJ3YWYgdGVzdCBD QTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKDEhcWKKhet2KgSdME+iEPxyEyn 2sd9IdElbt8DM2SfCdB2JsXo0C07UNZaywMPMfn/n8LNI/PKwu+N2uX7gfSjUzBR MB0GA1UdDgQWBBRZo3BPLv0KbV4drw6JI1JIUKmoRzAfBgNVHSMEGDAWgBRZo3BP Lv0KbV4drw6JI1JIUKmoRzAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0gA MEUCICC5k+76UpWoSwVbZA+atu5WcALEOJGqwOUWua3zemhcAiEA8Hfdxgwp0z2v rlG9y/jrJb6ORy3kTLWo2EA0BA67vuI= -----END CERTIFICATE----- ` // token is a token of 32 characters, the shortest allowed. const token = "0123456789abcdef0123456789abcdef" // off switches a timeout, a size limit or a rate limit off. const off = "off" // environment is a set of environment variables, for FromEnvironment. type environment map[string]string // lookupEnv reads one of the variables, as os.LookupEnv does. func (e environment) lookupEnv(name string) (string, bool) { value, ok := e[name] return value, ok } // fromEnvironment reads the settings from env, which must be valid. func fromEnvironment(t *testing.T, env environment) *config.Config { t.Helper() cfg, err := config.FromEnvironment(env.lookupEnv) if err != nil { t.Fatalf("settings %v: %v", env, err) } return cfg } func TestDefaults(t *testing.T) { t.Parallel() cfg := fromEnvironment(t, environment{}) wantSettings(t, cfg, config.Config{ ListenAddr: ":8080", Observe: false, ClientRequestTimeout: time.Minute, ClientRequestHeaderMaxBytes: 32 << 10, ClientIdleTimeout: 2 * time.Minute, ClientResponseTimeout: 30 * time.Minute, UpstreamRequestTimeout: time.Minute, UpstreamResponseTimeout: 30 * time.Minute, RequestMaxBytes: 100 << 20, ResponseMaxBytes: 5 << 30, RateLimitPerMinute: 1000, RateLimitPerHour: 10000, RateLimitPerDay: 50000, BanResponse: 403, LimitBanDuration: time.Hour, LimitBanRepeatWindow: 24 * time.Hour, MaxBanDuration: 7 * 24 * time.Hour, MaxBans: 5000, BanScopeV4Prefix: 32, StateDir: "/var/lib/smallwebwaf", StateWriteDelay: 10 * time.Second, StateCounterInterval: 15 * time.Minute, MetricsToken: "", MetricsTopN: 50, }) if cfg.UpstreamURL.String() != "http://127.0.0.1:8081" { t.Errorf("%s is %s", upstreamURL, cfg.UpstreamURL) } wantNetblocks(t, cfg.TrustedProxies, "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16") wantNetblocks(t, cfg.AllowNets) wantNetblocks(t, cfg.RateLimitExemptNets) wantNetblocks(t, cfg.DenyNets) wantCountries(t, deniedCountries, cfg.DeniedCountries) wantCountries(t, allowedCountries, cfg.ExclusivelyAllowedCountries) } func TestValuesAsSet(t *testing.T) { t.Parallel() cfg := fromEnvironment(t, environment{ listenAddr: "127.0.0.1:9000", upstreamURL: "https://app.internal:8443/", mode: "observe", trustedProxies: " 192.0.2.1, 10.1.2.3/8 ,2001:db8::/32", clientRequestTimeout: "90s", clientHeaderMaxBytes: "8K", clientIdleTimeout: "5m", clientResponseTimeout: "7d", upstreamRequestTimeout: "1h30m", upstreamResponseTimeout: off, requestMaxBytes: "512K", responseMaxBytes: "1234", allowNets: "192.0.2.7", rateLimitExemptNets: "2001:db8::/48, 10.9.8.7", denyNets: "198.51.100.0/24", rateLimitPerMinute: "60", rateLimitPerHour: "600", rateLimitPerDay: "6000", deniedCountries: "cn, RU,kp,Xk", allowedCountries: "de", banResponse: "429", limitBanDuration: "15m", limitBanRepeatWindow: "2d", maxBanDuration: "30d", maxBans: "100", banScopeV4Prefix: "24", stateDir: "/srv/waf-state", stateWriteDelay: "500ms", stateCounterInterval: "1h", metricsToken: token, metricsTopN: "10", }) wantSettings(t, cfg, config.Config{ ListenAddr: "127.0.0.1:9000", Observe: true, ClientRequestTimeout: 90 * time.Second, ClientRequestHeaderMaxBytes: 8 << 10, ClientIdleTimeout: 5 * time.Minute, ClientResponseTimeout: 7 * 24 * time.Hour, UpstreamRequestTimeout: 90 * time.Minute, UpstreamResponseTimeout: 0, RequestMaxBytes: 512 << 10, ResponseMaxBytes: 1234, RateLimitPerMinute: 60, RateLimitPerHour: 600, RateLimitPerDay: 6000, BanResponse: 429, LimitBanDuration: 15 * time.Minute, LimitBanRepeatWindow: 48 * time.Hour, MaxBanDuration: 30 * 24 * time.Hour, MaxBans: 100, BanScopeV4Prefix: 24, StateDir: "/srv/waf-state", StateWriteDelay: 500 * time.Millisecond, StateCounterInterval: time.Hour, MetricsToken: token, MetricsTopN: 10, }) if cfg.UpstreamURL.String() != "https://app.internal:8443/" { t.Errorf("%s is %s", upstreamURL, cfg.UpstreamURL) } wantNetblocks(t, cfg.TrustedProxies, "192.0.2.1/32", "10.0.0.0/8", "2001:db8::/32") wantNetblocks(t, cfg.AllowNets, "192.0.2.7/32") wantNetblocks(t, cfg.RateLimitExemptNets, "2001:db8::/48", "10.9.8.7/32") wantNetblocks(t, cfg.DenyNets, "198.51.100.0/24") wantCountries(t, deniedCountries, cfg.DeniedCountries, "CN", "RU", "KP", "XK") wantCountries(t, allowedCountries, cfg.ExclusivelyAllowedCountries, "DE") } func TestRemoteLogSettingsDefaults(t *testing.T) { t.Parallel() cfg := fromEnvironment(t, environment{}) hostname, _ := os.Hostname() if cfg.LogRemoteURL != nil || cfg.LogRemoteTLSCAs != nil || cfg.LogRemoteBuffer != 10000 || cfg.LogRemoteFacility != 16 || cfg.LogRemoteAppName != hostname { t.Errorf("remote log settings %v, %v, %d, %d and %q, want no URL, no "+ "certificates, 10000, 16 and %q", cfg.LogRemoteURL, cfg.LogRemoteTLSCAs, cfg.LogRemoteBuffer, cfg.LogRemoteFacility, cfg.LogRemoteAppName, hostname) } } func TestRemoteLogSettingsAsSet(t *testing.T) { t.Parallel() caFile := filepath.Join(t.TempDir(), "ca.pem") err := os.WriteFile(caFile, []byte(testCA), 0o600) if err != nil { t.Fatalf("write %s: %v", caFile, err) } cfg := fromEnvironment(t, environment{ logRemoteURL: "syslog+tls://logs.example:6514", logRemoteTLSCAFile: caFile, logRemoteBuffer: "500", logRemoteFacility: "daemon", logRemoteAppName: "fsn1app1/gitea", }) roots := x509.NewCertPool() roots.AppendCertsFromPEM([]byte(testCA)) if cfg.LogRemoteURL.String() != "syslog+tls://logs.example:6514" || !roots.Equal(cfg.LogRemoteTLSCAs) || cfg.LogRemoteBuffer != 500 || cfg.LogRemoteFacility != 3 || cfg.LogRemoteAppName != "fsn1app1/gitea" { t.Errorf("remote log settings %v, %v, %d, %d and %q", cfg.LogRemoteURL, cfg.LogRemoteTLSCAs, cfg.LogRemoteBuffer, cfg.LogRemoteFacility, cfg.LogRemoteAppName) } } func TestRemoteLogURLForms(t *testing.T) { t.Parallel() for _, value := range []string{ "syslog+udp://192.0.2.1:514", "syslog+tcp://[2001:db8::1]:514", "syslog+tls://logs.example:6514/", } { cfg := fromEnvironment(t, environment{logRemoteURL: value}) if cfg.LogRemoteURL.String() != value { t.Errorf("%s read as %v", value, cfg.LogRemoteURL) } } cfg := fromEnvironment(t, environment{logRemoteURL: ""}) if cfg.LogRemoteURL != nil { t.Errorf("set but empty, %s read as %v", logRemoteURL, cfg.LogRemoteURL) } } func TestRemoteLogFacilitiesByNumber(t *testing.T) { t.Parallel() for name, number := range map[string]int{ "kern": 0, "user": 1, "auth": 4, "authpriv": 10, "ftp": 11, "local0": 16, "local5": 21, "local7": 23, } { cfg := fromEnvironment(t, environment{logRemoteFacility: name}) if cfg.LogRemoteFacility != number { t.Errorf("%s read as %d, want %d", name, cfg.LogRemoteFacility, number) } } } func TestInvalidRemoteLogSettingStopsTheStart(t *testing.T) { t.Parallel() for _, tc := range []struct{ name, value string }{ {logRemoteURL, "logs.example:514"}, {logRemoteURL, "syslog://logs.example:514"}, {logRemoteURL, "http://logs.example:514"}, {logRemoteURL, "syslog+udp://logs.example"}, {logRemoteURL, "syslog+tcp://:514"}, {logRemoteURL, "syslog+tcp://logs.example:0"}, {logRemoteURL, "syslog+tls://logs.example:65536"}, {logRemoteURL, "syslog+tls://user@logs.example:6514"}, {logRemoteURL, "syslog+tcp://logs.example:514/app"}, {logRemoteURL, "syslog+tcp://logs.example:514?tls=1"}, {logRemoteTLSCAFile, "/nonexistent/ca.pem"}, {logRemoteBuffer, off}, {logRemoteBuffer, "0"}, {logRemoteBuffer, "10K"}, {logRemoteFacility, "local8"}, {logRemoteFacility, "LOCAL0"}, {logRemoteFacility, "16"}, {logRemoteFacility, ""}, {logRemoteAppName, ""}, {logRemoteAppName, "my app"}, {logRemoteAppName, "gitéa"}, {logRemoteAppName, strings.Repeat("a", 49)}, } { _, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv) if err == nil || !strings.HasPrefix(err.Error(), tc.name+": ") { t.Errorf("%s=%q: error %v, want one naming it", tc.name, tc.value, err) } } } func TestRemoteLogCAFileWithoutCertificateStopsTheStart(t *testing.T) { t.Parallel() caFile := filepath.Join(t.TempDir(), "ca.pem") err := os.WriteFile(caFile, []byte("not a certificate\n"), 0o600) if err != nil { t.Fatalf("write %s: %v", caFile, err) } _, err = config.FromEnvironment(environment{logRemoteTLSCAFile: caFile}.lookupEnv) want := logRemoteTLSCAFile + `: "` + caFile + `" holds no PEM certificate` if err == nil || err.Error() != want { t.Errorf("error %v, want %s", err, want) } } func TestCodeOnBothCountryListsStopsTheStart(t *testing.T) { t.Parallel() _, err := config.FromEnvironment(environment{ deniedCountries: "cn,ru", allowedCountries: "de,RU", }.lookupEnv) if err == nil { t.Fatal("ru on both country lists was accepted") } if !strings.HasPrefix(err.Error(), allowedCountries+": ") || !strings.Contains(err.Error(), `"RU"`) { t.Errorf("error %q does not name %s and RU", err, allowedCountries) } } func TestSizesAndOff(t *testing.T) { t.Parallel() cfg := fromEnvironment(t, environment{ requestMaxBytes: "3G", responseMaxBytes: off, clientRequestTimeout: off, clientIdleTimeout: off, }) if cfg.RequestMaxBytes != 3<<30 || cfg.ResponseMaxBytes != 0 || cfg.ClientRequestTimeout != 0 || cfg.ClientIdleTimeout != 0 { t.Errorf("3G, off, off and off read as %d, %d, %s and %s", cfg.RequestMaxBytes, cfg.ResponseMaxBytes, cfg.ClientRequestTimeout, cfg.ClientIdleTimeout) } } func TestRequestHeaderMaxBytesJustOver4K(t *testing.T) { t.Parallel() cfg := fromEnvironment(t, environment{clientHeaderMaxBytes: "4097"}) if cfg.ClientRequestHeaderMaxBytes != 4097 { t.Errorf("4097 read as %d", cfg.ClientRequestHeaderMaxBytes) } } func TestRequestHeaderMaxBytesRefusalNeverOffersOff(t *testing.T) { t.Parallel() for _, value := range []string{"32KB", "0", "4K", off} { t.Run(value, func(t *testing.T) { t.Parallel() _, err := config.FromEnvironment( environment{clientHeaderMaxBytes: value}.lookupEnv) want := clientHeaderMaxBytes + `: "` + value + `" is not a size of more than 4K, such as 32K` if err == nil || err.Error() != want { t.Errorf("error %v, want %s", err, want) } }) } } func TestRateLimitsOff(t *testing.T) { t.Parallel() cfg := fromEnvironment(t, environment{ rateLimitPerMinute: off, rateLimitPerHour: off, rateLimitPerDay: off, }) if cfg.RateLimitPerMinute != 0 || cfg.RateLimitPerHour != 0 || cfg.RateLimitPerDay != 0 { t.Errorf("off read as %d, %d and %d", cfg.RateLimitPerMinute, cfg.RateLimitPerHour, cfg.RateLimitPerDay) } } func TestBanResponseCloseIsZero(t *testing.T) { t.Parallel() cfg := fromEnvironment(t, environment{banResponse: "close"}) if cfg.BanResponse != 0 { t.Errorf("close read as %d, want 0", cfg.BanResponse) } } func TestTrustedProxiesSetButEmptyTrustNothing(t *testing.T) { t.Parallel() cfg := fromEnvironment(t, environment{trustedProxies: ""}) if len(cfg.TrustedProxies) != 0 { t.Errorf("trusted proxies %v, want none", cfg.TrustedProxies) } } func TestInvalidValueStopsTheStart(t *testing.T) { t.Parallel() for _, tc := range []struct{ name, value string }{ {listenAddr, "8080"}, {listenAddr, ":http"}, {listenAddr, ":65536"}, {upstreamURL, "127.0.0.1:8081"}, {upstreamURL, "ftp://127.0.0.1:8081"}, {upstreamURL, "http://"}, {upstreamURL, "http://:8081"}, {upstreamURL, "http://127.0.0.1:0"}, {upstreamURL, "http://127.0.0.1:99999"}, {upstreamURL, "http://127.0.0.1:8081/app"}, {upstreamURL, "http://127.0.0.1:8081/?a=1"}, {upstreamURL, "http://user:secret@127.0.0.1:8081"}, {mode, "Observe"}, {mode, "block"}, {mode, ""}, {trustedProxies, "10.0.0.0/33"}, {trustedProxies, "traefik"}, {trustedProxies, "10.0.0.0/8,,192.168.0.0/16"}, {trustedProxies, "fe80::1%eth0"}, {allowNets, "192.0.2.0/24,monitoring"}, {rateLimitExemptNets, "2001:db8::/129"}, {denyNets, "198.51.100.0/24,"}, {clientRequestTimeout, "60"}, {clientRequestTimeout, ""}, {clientIdleTimeout, "0s"}, {clientIdleTimeout, "2 minutes"}, {clientResponseTimeout, "1y"}, {upstreamRequestTimeout, "-1s"}, {upstreamResponseTimeout, "0s"}, {upstreamResponseTimeout, "1.5d"}, {requestMaxBytes, "100MB"}, {requestMaxBytes, "100m"}, {requestMaxBytes, "1.5M"}, {responseMaxBytes, "0"}, {responseMaxBytes, "-5"}, {responseMaxBytes, "99999999999G"}, {rateLimitPerMinute, ""}, {rateLimitPerMinute, "1K"}, {rateLimitPerHour, "0"}, {rateLimitPerHour, "1.5"}, {rateLimitPerDay, "-1"}, {rateLimitPerDay, "lots"}, {deniedCountries, "nk"}, {deniedCountries, "kp,,ir"}, {deniedCountries, "prk"}, {deniedCountries, "408"}, {deniedCountries, "k"}, {deniedCountries, "eu"}, {deniedCountries, "un"}, {deniedCountries, "su"}, {allowedCountries, "ac"}, {allowedCountries, "uk"}, {allowedCountries, "zz"}, {allowedCountries, "de,germany"}, {banResponse, "404"}, {banResponse, "drop"}, {banResponse, ""}, {limitBanDuration, off}, {limitBanDuration, "0s"}, {limitBanDuration, "1"}, {limitBanRepeatWindow, off}, {limitBanRepeatWindow, "-1h"}, {maxBanDuration, off}, {maxBanDuration, "1w"}, {maxBans, off}, {maxBans, "0"}, {maxBans, "5K"}, {banScopeV4Prefix, "33"}, {banScopeV4Prefix, "-1"}, {banScopeV4Prefix, "/24"}, {stateDir, ""}, {stateDir, "state"}, {stateDir, "./var/lib/smallwebwaf"}, {stateWriteDelay, off}, {stateWriteDelay, "0s"}, {stateCounterInterval, off}, {stateCounterInterval, "15"}, {metricsTopN, off}, {metricsTopN, "0"}, {metricsTopN, "-1"}, } { t.Run(tc.name+"="+tc.value, func(t *testing.T) { t.Parallel() _, err := config.FromEnvironment(environment{tc.name: tc.value}.lookupEnv) if err == nil { t.Fatalf("%s=%q was accepted", tc.name, tc.value) } if !strings.HasPrefix(err.Error(), tc.name+": ") { t.Errorf("error %q does not name %s", err, tc.name) } }) } } func TestShortTokenStopsTheStartWithoutShowingIt(t *testing.T) { t.Parallel() // Characters are counted, not bytes: each é takes two. for _, value := range []string{"", token[1:], strings.Repeat("é", 31)} { t.Run(value, func(t *testing.T) { t.Parallel() _, err := config.FromEnvironment(environment{metricsToken: value}.lookupEnv) want := metricsToken + ": is shorter than 32 characters" if err == nil || err.Error() != want { t.Errorf("error %v, want %s", err, want) } }) } } func TestTokenIsLoggedMasked(t *testing.T) { t.Parallel() cfg := fromEnvironment(t, environment{metricsToken: token}) var out bytes.Buffer slog.New(slog.NewJSONHandler(&out, nil)).Info("starting", "settings", cfg) if strings.Contains(out.String(), token) || !strings.Contains(out.String(), `"`+metricsToken+`":"********"`) { t.Errorf("the token is not logged masked: %s", out.String()) } } func TestLogsEachSettingWithItsValue(t *testing.T) { t.Parallel() cfg := fromEnvironment(t, environment{clientRequestTimeout: "45s"}) var out bytes.Buffer slog.New(slog.NewJSONHandler(&out, nil)).Info("starting", "settings", cfg) var line struct { Settings map[string]string `json:"settings"` } err := json.Unmarshal(out.Bytes(), &line) if err != nil { t.Fatalf("decode %s: %v", out.Bytes(), err) } hostname, _ := os.Hostname() want := map[string]string{ listenAddr: ":8080", upstreamURL: "http://127.0.0.1:8081", mode: "enforce", trustedProxies: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16", clientRequestTimeout: "45s", clientHeaderMaxBytes: "32K", clientIdleTimeout: "120s", clientResponseTimeout: "30m", upstreamRequestTimeout: "60s", upstreamResponseTimeout: "30m", requestMaxBytes: "100M", responseMaxBytes: "5G", allowNets: "", rateLimitExemptNets: "", denyNets: "", rateLimitPerMinute: "1000", rateLimitPerHour: "10000", rateLimitPerDay: "50000", deniedCountries: "", allowedCountries: "", banResponse: "403", limitBanDuration: "1h", limitBanRepeatWindow: "24h", maxBanDuration: "7d", maxBans: "5000", banScopeV4Prefix: "32", stateDir: "/var/lib/smallwebwaf", stateWriteDelay: "10s", stateCounterInterval: "15m", metricsToken: "", metricsTopN: "50", logRemoteURL: "", logRemoteTLSCAFile: "", logRemoteBuffer: "10000", logRemoteFacility: "local0", logRemoteAppName: hostname, } if !maps.Equal(line.Settings, want) { t.Errorf("logged settings\n%v\nwant\n%v", line.Settings, want) } } // wantSettings checks the settings that are plain values. func wantSettings(t *testing.T, got *config.Config, want config.Config) { t.Helper() if got.ListenAddr != want.ListenAddr || got.Observe != want.Observe || got.ClientRequestTimeout != want.ClientRequestTimeout || got.ClientRequestHeaderMaxBytes != want.ClientRequestHeaderMaxBytes || got.ClientIdleTimeout != want.ClientIdleTimeout || got.ClientResponseTimeout != want.ClientResponseTimeout || got.UpstreamRequestTimeout != want.UpstreamRequestTimeout || got.UpstreamResponseTimeout != want.UpstreamResponseTimeout || got.RequestMaxBytes != want.RequestMaxBytes || got.ResponseMaxBytes != want.ResponseMaxBytes || got.RateLimitPerMinute != want.RateLimitPerMinute || got.RateLimitPerHour != want.RateLimitPerHour || got.RateLimitPerDay != want.RateLimitPerDay { t.Errorf("settings\n%+v\nwant\n%+v", got, want) } wantBanSettings(t, got, want) } // wantBanSettings checks the settings for bans, the state files and the // metrics. func wantBanSettings(t *testing.T, got *config.Config, want config.Config) { t.Helper() if got.BanResponse != want.BanResponse || got.LimitBanDuration != want.LimitBanDuration || got.LimitBanRepeatWindow != want.LimitBanRepeatWindow || got.MaxBanDuration != want.MaxBanDuration || got.MaxBans != want.MaxBans || got.BanScopeV4Prefix != want.BanScopeV4Prefix { t.Errorf("ban settings\n%+v\nwant\n%+v", got, want) } if got.StateDir != want.StateDir || got.StateWriteDelay != want.StateWriteDelay || got.StateCounterInterval != want.StateCounterInterval { t.Errorf("state settings\n%+v\nwant\n%+v", got, want) } if got.MetricsToken != want.MetricsToken || got.MetricsTopN != want.MetricsTopN { t.Errorf("metrics token %q and top %d, want %q and %d", got.MetricsToken, got.MetricsTopN, want.MetricsToken, want.MetricsTopN) } } // wantNetblocks checks a list of netblocks. func wantNetblocks(t *testing.T, got []netip.Prefix, want ...string) { t.Helper() gotText := make([]string, 0, len(got)) for _, netblock := range got { gotText = append(gotText, netblock.String()) } if !slices.Equal(gotText, want) { t.Errorf("netblocks %v, want %v", gotText, want) } } // wantCountries checks the list of countries the setting name gave. func wantCountries(t *testing.T, name string, got []string, want ...string) { t.Helper() if !slices.Equal(got, want) { t.Errorf("%s gave %v, want %v", name, got, want) } }