check / check (push) Waiting to run
DEBUG was read with strconv.ParseBool, so a value such as "yes" stopped the start. It now follows the code style guide: unset or empty is off, and any other value, "false" and "0" included, turns debug logging on. No value of DEBUG stops the start. The test that "yes" fails is replaced by one covering unset, empty, "false", "0" and "yes". README.md no longer gives false as the default, and says which values turn it on. Model: opus-5-5
246 lines
6.6 KiB
Go
246 lines
6.6 KiB
Go
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")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDebugIsOnForAnyNonEmptyValue: unset or empty, debug logging is
|
|
// off; any other value turns it on, "false" and "0" included, and none
|
|
// stops the start.
|
|
func TestDebugIsOnForAnyNonEmptyValue(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
value string
|
|
unset bool
|
|
want bool
|
|
}{
|
|
{name: "unset", unset: true, want: false},
|
|
{name: "empty", value: "", want: false},
|
|
{name: "false", value: "false", want: true},
|
|
{name: "0", value: "0", want: true},
|
|
{name: "yes", value: "yes", want: true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// t.Setenv first, so the variable is put back as it was
|
|
// after the test.
|
|
t.Setenv("DEBUG", tc.value)
|
|
|
|
if tc.unset {
|
|
err := os.Unsetenv("DEBUG")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
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.Debug != tc.want {
|
|
t.Fatalf("Debug = %t, want %t", cfg.Debug, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
})
|
|
}
|
|
}
|