check / check (push) Canceled after 0s
PORT and DNSWATCHER_TLS_EXPIRY_WARNING were read with viper.GetInt, and DNSWATCHER_DEBUG, DNSWATCHER_MAINTENANCE_MODE and DNSWATCHER_SEND_TEST_NOTIFICATION with viper.GetBool, which turn a value that does not parse into 0 or false with no error. Each is now parsed the way parseInterval parses an interval: PORT must be 1 to 65535, the expiry warning 1 or more days, the true/false settings a value strconv.ParseBool accepts. Anything else stops startup with an error naming the variable and the value; empty still means the default. Reading the config file moved, unchanged, from buildConfig into readConfigFile, so buildConfig stays within the linter's length limit. Model: opus-5-5
390 lines
11 KiB
Go
390 lines
11 KiB
Go
package config_test
|
|
|
|
import (
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/viper"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"sneak.berlin/go/dnswatcher/internal/config"
|
|
"sneak.berlin/go/dnswatcher/internal/globals"
|
|
"sneak.berlin/go/dnswatcher/internal/logger"
|
|
)
|
|
|
|
// newTestParams creates config.Params suitable for testing
|
|
// without requiring the fx dependency injection framework.
|
|
func newTestParams(t *testing.T) config.Params {
|
|
t.Helper()
|
|
|
|
g := &globals.Globals{
|
|
Appname: "dnswatcher",
|
|
Version: "test",
|
|
}
|
|
|
|
l, err := logger.New(nil, logger.Params{Globals: g})
|
|
require.NoError(t, err, "failed to create logger")
|
|
|
|
return config.Params{
|
|
Globals: g,
|
|
Logger: l,
|
|
}
|
|
}
|
|
|
|
// These tests exercise viper global state and MUST NOT use
|
|
// t.Parallel(). Each test resets viper for isolation.
|
|
|
|
func TestNew_DefaultValues(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com,www.example.com")
|
|
|
|
cfg, err := config.New(nil, newTestParams(t))
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 8080, cfg.Port)
|
|
assert.False(t, cfg.Debug)
|
|
assert.Equal(t, "/var/lib/dnswatcher", cfg.DataDir)
|
|
assert.Equal(t, time.Hour, cfg.DNSInterval)
|
|
assert.Equal(t, 12*time.Hour, cfg.TLSInterval)
|
|
assert.Equal(t, 7, cfg.TLSExpiryWarning)
|
|
assert.False(t, cfg.MaintenanceMode)
|
|
assert.Empty(t, cfg.SlackWebhook)
|
|
assert.Empty(t, cfg.MattermostWebhook)
|
|
assert.Empty(t, cfg.NtfyTopic)
|
|
assert.Empty(t, cfg.SentryDSN)
|
|
assert.Empty(t, cfg.MetricsUsername)
|
|
assert.Empty(t, cfg.MetricsPassword)
|
|
assert.False(t, cfg.SendTestNotification)
|
|
assert.Empty(t, cfg.SkipRecordNotifications)
|
|
}
|
|
|
|
func TestNew_EnvironmentOverrides(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
|
t.Setenv("PORT", "9090")
|
|
t.Setenv("DNSWATCHER_DEBUG", "true")
|
|
t.Setenv("DNSWATCHER_DATA_DIR", "/tmp/test-data")
|
|
t.Setenv("DNSWATCHER_DNS_INTERVAL", "30m")
|
|
t.Setenv("DNSWATCHER_TLS_INTERVAL", "6h")
|
|
t.Setenv("DNSWATCHER_TLS_EXPIRY_WARNING", "14")
|
|
t.Setenv("DNSWATCHER_SLACK_WEBHOOK", "https://hooks.slack.com/t")
|
|
t.Setenv("DNSWATCHER_MATTERMOST_WEBHOOK", "https://mm.test/hooks/t")
|
|
t.Setenv("DNSWATCHER_NTFY_TOPIC", "https://ntfy.sh/test")
|
|
t.Setenv("DNSWATCHER_SENTRY_DSN", "https://sentry.test/1")
|
|
t.Setenv("DNSWATCHER_MAINTENANCE_MODE", "true")
|
|
t.Setenv("DNSWATCHER_METRICS_USERNAME", "admin")
|
|
t.Setenv("DNSWATCHER_METRICS_PASSWORD", "secret")
|
|
t.Setenv("DNSWATCHER_SEND_TEST_NOTIFICATION", "true")
|
|
|
|
cfg, err := config.New(nil, newTestParams(t))
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 9090, cfg.Port)
|
|
assert.True(t, cfg.Debug)
|
|
assert.Equal(t, "/tmp/test-data", cfg.DataDir)
|
|
assert.Equal(t, 30*time.Minute, cfg.DNSInterval)
|
|
assert.Equal(t, 6*time.Hour, cfg.TLSInterval)
|
|
assert.Equal(t, 14, cfg.TLSExpiryWarning)
|
|
assert.Equal(t, "https://hooks.slack.com/t", cfg.SlackWebhook)
|
|
assert.Equal(t, "https://mm.test/hooks/t", cfg.MattermostWebhook)
|
|
assert.Equal(t, "https://ntfy.sh/test", cfg.NtfyTopic)
|
|
assert.Equal(t, "https://sentry.test/1", cfg.SentryDSN)
|
|
assert.True(t, cfg.MaintenanceMode)
|
|
assert.Equal(t, "admin", cfg.MetricsUsername)
|
|
assert.Equal(t, "secret", cfg.MetricsPassword)
|
|
assert.True(t, cfg.SendTestNotification)
|
|
}
|
|
|
|
func TestNew_NoTargetsError(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "")
|
|
|
|
_, err := config.New(nil, newTestParams(t))
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, config.ErrNoTargets)
|
|
}
|
|
|
|
func TestNew_OnlyEmptyCSVSegments(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", " , , ")
|
|
|
|
_, err := config.New(nil, newTestParams(t))
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, config.ErrNoTargets)
|
|
}
|
|
|
|
// TestNew_InvalidIntervalStopsStartup checks values that must stop startup;
|
|
// TestNew_DefaultValues and TestNew_EmptyValueMeansDefault check that an
|
|
// unset or empty interval means the default.
|
|
func TestNew_InvalidIntervalStopsStartup(t *testing.T) {
|
|
variables := []string{"DNSWATCHER_DNS_INTERVAL", "DNSWATCHER_TLS_INTERVAL"}
|
|
values := []string{
|
|
"banana", // not a duration
|
|
"5", // no unit
|
|
"1d", // days are not a unit time.ParseDuration knows
|
|
"0", // zero
|
|
"-1h", // negative
|
|
}
|
|
|
|
for _, variable := range variables {
|
|
for _, value := range values {
|
|
t.Run(variable+"="+value, func(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
|
t.Setenv(variable, value)
|
|
|
|
_, err := config.New(nil, newTestParams(t))
|
|
require.ErrorIs(t, err, config.ErrInvalidInterval)
|
|
require.ErrorContains(t, err, variable)
|
|
require.ErrorContains(t, err, strconv.Quote(value))
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNew_EmptyValueMeansDefault(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
|
t.Setenv("DNSWATCHER_DNS_INTERVAL", "")
|
|
t.Setenv("DNSWATCHER_TLS_INTERVAL", "")
|
|
t.Setenv("PORT", "")
|
|
t.Setenv("DNSWATCHER_TLS_EXPIRY_WARNING", "")
|
|
t.Setenv("DNSWATCHER_DEBUG", "")
|
|
t.Setenv("DNSWATCHER_MAINTENANCE_MODE", "")
|
|
t.Setenv("DNSWATCHER_SEND_TEST_NOTIFICATION", "")
|
|
|
|
cfg, err := config.New(nil, newTestParams(t))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, time.Hour, cfg.DNSInterval)
|
|
assert.Equal(t, 12*time.Hour, cfg.TLSInterval)
|
|
assert.Equal(t, 8080, cfg.Port)
|
|
assert.Equal(t, 7, cfg.TLSExpiryWarning)
|
|
assert.False(t, cfg.Debug)
|
|
assert.False(t, cfg.MaintenanceMode)
|
|
assert.False(t, cfg.SendTestNotification)
|
|
}
|
|
|
|
// TestNew_InvalidNumberOrBoolStopsStartup checks values that must stop
|
|
// startup; TestNew_EnvironmentOverrides, the two tests after this one and
|
|
// TestNew_EmptyValueMeansDefault check values that must not.
|
|
func TestNew_InvalidNumberOrBoolStopsStartup(t *testing.T) {
|
|
notBools := []string{"yes", "on", "2", "tRUE"}
|
|
tests := []struct {
|
|
variable string
|
|
values []string
|
|
want error
|
|
}{
|
|
{
|
|
variable: "PORT",
|
|
values: []string{"notaport", "0", "-1", "65536", "8080.5"},
|
|
want: config.ErrInvalidPort,
|
|
},
|
|
{
|
|
variable: "DNSWATCHER_TLS_EXPIRY_WARNING",
|
|
values: []string{"abc", "7d", "0", "-5"},
|
|
want: config.ErrInvalidTLSExpiryWarning,
|
|
},
|
|
{
|
|
variable: "DNSWATCHER_DEBUG",
|
|
values: notBools,
|
|
want: config.ErrInvalidBool,
|
|
},
|
|
{
|
|
variable: "DNSWATCHER_MAINTENANCE_MODE",
|
|
values: notBools,
|
|
want: config.ErrInvalidBool,
|
|
},
|
|
{
|
|
variable: "DNSWATCHER_SEND_TEST_NOTIFICATION",
|
|
values: notBools,
|
|
want: config.ErrInvalidBool,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
for _, value := range tt.values {
|
|
t.Run(tt.variable+"="+value, func(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
|
t.Setenv(tt.variable, value)
|
|
|
|
_, err := config.New(nil, newTestParams(t))
|
|
require.ErrorIs(t, err, tt.want)
|
|
require.ErrorContains(t, err, tt.variable)
|
|
require.ErrorContains(t, err, strconv.Quote(value))
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNew_LowestNumbersAndOtherSpellingsOfTrue(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
|
t.Setenv("PORT", "1")
|
|
t.Setenv("DNSWATCHER_TLS_EXPIRY_WARNING", "1")
|
|
t.Setenv("DNSWATCHER_DEBUG", "1")
|
|
t.Setenv("DNSWATCHER_MAINTENANCE_MODE", "T")
|
|
t.Setenv("DNSWATCHER_SEND_TEST_NOTIFICATION", "True")
|
|
|
|
cfg, err := config.New(nil, newTestParams(t))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, cfg.Port)
|
|
assert.Equal(t, 1, cfg.TLSExpiryWarning)
|
|
assert.True(t, cfg.Debug)
|
|
assert.True(t, cfg.MaintenanceMode)
|
|
assert.True(t, cfg.SendTestNotification)
|
|
}
|
|
|
|
func TestNew_HighestPort(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
|
t.Setenv("PORT", "65535")
|
|
|
|
cfg, err := config.New(nil, newTestParams(t))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 65535, cfg.Port)
|
|
}
|
|
|
|
func TestNew_DebugEnablesDebugLogging(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
|
t.Setenv("DNSWATCHER_DEBUG", "true")
|
|
|
|
cfg, err := config.New(nil, newTestParams(t))
|
|
require.NoError(t, err)
|
|
assert.True(t, cfg.Debug)
|
|
}
|
|
|
|
func TestNew_PortEnvNotPrefixed(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
|
t.Setenv("PORT", "3000")
|
|
|
|
cfg, err := config.New(nil, newTestParams(t))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 3000, cfg.Port,
|
|
"PORT env should work without DNSWATCHER_ prefix")
|
|
}
|
|
|
|
func TestNew_TargetClassification(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS",
|
|
"example.com,www.example.com,api.example.com,example.org")
|
|
|
|
cfg, err := config.New(nil, newTestParams(t))
|
|
require.NoError(t, err)
|
|
|
|
// example.com and example.org are apex domains
|
|
assert.Len(t, cfg.Domains, 2)
|
|
// www.example.com and api.example.com are hostnames
|
|
assert.Len(t, cfg.Hostnames, 2)
|
|
}
|
|
|
|
func TestNew_InvalidTargetPublicSuffix(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "co.uk")
|
|
|
|
_, err := config.New(nil, newTestParams(t))
|
|
require.Error(t, err, "public suffix should be rejected")
|
|
}
|
|
|
|
func TestNew_EmptyAppnameDefaultsToDnswatcher(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
|
|
|
g := &globals.Globals{Appname: "", Version: "test"}
|
|
|
|
l, err := logger.New(nil, logger.Params{Globals: g})
|
|
require.NoError(t, err)
|
|
|
|
cfg, err := config.New(
|
|
nil, config.Params{Globals: g, Logger: l},
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 8080, cfg.Port,
|
|
"defaults should load when appname is empty")
|
|
}
|
|
|
|
func TestNew_TargetsWithWhitespace(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", " example.com , www.example.com ")
|
|
|
|
cfg, err := config.New(nil, newTestParams(t))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 2, len(cfg.Domains)+len(cfg.Hostnames),
|
|
"whitespace around targets should be trimmed")
|
|
}
|
|
|
|
func TestNew_TargetsWithTrailingComma(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com,www.example.com,")
|
|
|
|
cfg, err := config.New(nil, newTestParams(t))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 2, len(cfg.Domains)+len(cfg.Hostnames),
|
|
"trailing comma should be ignored")
|
|
}
|
|
|
|
func TestNew_SkipRecordNotifications(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.net,www.example.net,example.org")
|
|
t.Setenv("DNSWATCHER_SKIP_RECORD_NOTIFICATIONS",
|
|
" WWW.Example.net. , example.net,www.example.net")
|
|
|
|
cfg, err := config.New(nil, newTestParams(t))
|
|
require.NoError(t, err)
|
|
assert.Equal(t,
|
|
[]string{"www.example.net", "example.net"},
|
|
cfg.SkipRecordNotifications,
|
|
"names are written as targets are, each once",
|
|
)
|
|
}
|
|
|
|
func TestNew_SkipRecordNotificationsNotInTargetsStopsStartup(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.net")
|
|
t.Setenv("DNSWATCHER_SKIP_RECORD_NOTIFICATIONS",
|
|
"example.net,www.example.net")
|
|
|
|
_, err := config.New(nil, newTestParams(t))
|
|
require.ErrorIs(t, err, config.ErrNotInTargets)
|
|
require.ErrorContains(t, err, "DNSWATCHER_SKIP_RECORD_NOTIFICATIONS")
|
|
require.ErrorContains(t, err, `"www.example.net"`)
|
|
}
|
|
|
|
func TestNew_CustomDNSIntervalDuration(t *testing.T) {
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
|
t.Setenv("DNSWATCHER_DNS_INTERVAL", "5s")
|
|
|
|
cfg, err := config.New(nil, newTestParams(t))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 5*time.Second, cfg.DNSInterval)
|
|
}
|
|
|
|
func TestStatePath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
dataDir string
|
|
want string
|
|
}{
|
|
{"default", "/var/lib/dnswatcher", "/var/lib/dnswatcher/state.json"},
|
|
{"absolute", "/var/lib/dw", "/var/lib/dw/state.json"},
|
|
{"nested", "/opt/app/data", "/opt/app/data/state.json"},
|
|
{"empty", "", "/state.json"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := &config.Config{DataDir: tt.dataDir}
|
|
assert.Equal(t, tt.want, cfg.StatePath())
|
|
})
|
|
}
|
|
}
|