package config_test import ( "os" "slices" "strings" "testing" "sneak.berlin/go/netwatch/internal/config" "sneak.berlin/go/netwatch/internal/globals" "sneak.berlin/go/netwatch/internal/logger" "go.uber.org/fx" ) // requireConfigError builds the config as main does and fails the // test unless that fails with an error naming setting. It uses // fx.New, because fxtest.New fails the test itself on an error. func requireConfigError(t *testing.T, setting string) { t.Helper() app := fx.New( fx.NopLogger, fx.Provide(globals.New, logger.New, config.New), fx.Invoke(func(*config.Config) {}), ) err := app.Err() if err == nil || !strings.Contains(err.Error(), setting) { t.Fatalf("config error = %v, want one naming %s", err, setting) } } // requireTrustedProxies builds the config as main does and fails the // test unless its TrustedProxies are want. func requireTrustedProxies(t *testing.T, want []string) { t.Helper() var cfg *config.Config app := fx.New( fx.NopLogger, fx.Provide(globals.New, logger.New, config.New), fx.Populate(&cfg), ) err := app.Err() if err != nil { t.Fatalf("config error = %v", err) } if !slices.Equal(cfg.TrustedProxies, want) { t.Fatalf("TrustedProxies = %q, want %q", cfg.TrustedProxies, want) } } // TestTrustedProxiesUnsetTrustsRFC1918: unset, the server trusts the // RFC 1918 ranges and nothing else, loopback included. func TestTrustedProxiesUnsetTrustsRFC1918(t *testing.T) { // t.Setenv first, so the variable is put back as it was after the // test. t.Setenv("TRUSTED_PROXIES", "") err := os.Unsetenv("TRUSTED_PROXIES") if err != nil { t.Fatal(err) } requireTrustedProxies(t, []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}) } // TestTrustedProxiesEmptyTrustsNothing: set and empty, the server // trusts no proxy. viper alone would read the empty variable as unset // and give the default. func TestTrustedProxiesEmptyTrustsNothing(t *testing.T) { t.Setenv("TRUSTED_PROXIES", "") requireTrustedProxies(t, []string{}) } // TestTrustedProxiesListReplacesDefault: a list is used as given, with // none of the default ranges added. func TestTrustedProxiesListReplacesDefault(t *testing.T) { t.Setenv("TRUSTED_PROXIES", "172.17.0.1/32, 2001:db8::/32") requireTrustedProxies(t, []string{"172.17.0.1/32", "2001:db8::/32"}) } // TestSettingsLoadAsGiven: valid values pass the checks and are used // as given. bin/entrypoint.sh starts the server with these // BIND_ADDRESS and PORT values. func TestSettingsLoadAsGiven(t *testing.T) { t.Setenv("BIND_ADDRESS", "127.0.0.1") t.Setenv("PORT", "8081") t.Setenv("DEBUG", "true") var cfg *config.Config app := fx.New( fx.NopLogger, fx.Provide(globals.New, logger.New, config.New), fx.Populate(&cfg), ) err := app.Err() if err != nil { t.Fatalf("config error = %v", err) } if cfg.BindAddress != "127.0.0.1" || cfg.Port != 8081 || !cfg.Debug { t.Fatalf("BindAddress, Port, Debug = %q, %d, %t; "+ "want \"127.0.0.1\", 8081, true", cfg.BindAddress, cfg.Port, cfg.Debug) } } // TestPortMustBeAPortNumber: viper reads a value that is not a number // as 0, on which the server would listen on a random port. func TestPortMustBeAPortNumber(t *testing.T) { for _, value := range []string{"abc", "0", "65536", "8080.5"} { t.Run(value, func(t *testing.T) { t.Setenv("PORT", value) requireConfigError(t, "PORT") }) } } // TestDebugMustBeTrueOrFalse: viper reads any other value, such as // "yes", as false. func TestDebugMustBeTrueOrFalse(t *testing.T) { t.Setenv("DEBUG", "yes") requireConfigError(t, "DEBUG") } // TestBindAddressMustBeAnIPAddress: a host name would be looked up // only once the server starts listening, and a mistyped one would stop // it then with an error that does not name the setting. func TestBindAddressMustBeAnIPAddress(t *testing.T) { t.Setenv("BIND_ADDRESS", "localhost") requireConfigError(t, "BIND_ADDRESS") } // TestReportsPerMinuteMustBePositive: unchecked, zero would panic // when the routes are built, and a negative rate would lift the // limit. func TestReportsPerMinuteMustBePositive(t *testing.T) { t.Setenv("REPORTS_PER_MINUTE", "0") requireConfigError(t, "REPORTS_PER_MINUTE") } // TestDataDirMaxBytesMustBeANumber: viper reads a value that is not // a number, such as "1GB", as 0, which would refuse every report. func TestDataDirMaxBytesMustBeANumber(t *testing.T) { t.Setenv("DATA_DIR_MAX_BYTES", "1GB") requireConfigError(t, "DATA_DIR_MAX_BYTES") } // TestMetricsCredentialsGoTogether: with only one of the two set, the // server would quietly serve no metrics, so the start fails, naming // both. func TestMetricsCredentialsGoTogether(t *testing.T) { for _, set := range []string{"METRICS_USERNAME", "METRICS_PASSWORD"} { t.Run(set, func(t *testing.T) { t.Setenv("METRICS_USERNAME", "") t.Setenv("METRICS_PASSWORD", "") t.Setenv(set, "prometheus") requireConfigError(t, "METRICS_USERNAME") requireConfigError(t, "METRICS_PASSWORD") }) } } // TestMetricsUsernameMustNotContainColon: basic auth splits the // credentials at the first ":", so such a user name would get 401 on // every request to /metrics. func TestMetricsUsernameMustNotContainColon(t *testing.T) { t.Setenv("METRICS_USERNAME", "prom:etheus") t.Setenv("METRICS_PASSWORD", "secret") requireConfigError(t, "METRICS_USERNAME") } // TestCORSAllowedOriginsMustBeOrigins: "*" would let every origin in, // and an entry that is not a plain origin would match no page. func TestCORSAllowedOriginsMustBeOrigins(t *testing.T) { for _, entry := range []string{ "*", "https://*.netwatch.example", "netwatch.example", "https://netwatch.example/", } { t.Run(entry, func(t *testing.T) { t.Setenv("CORS_ALLOWED_ORIGINS", entry) requireConfigError(t, "CORS_ALLOWED_ORIGINS") }) } }