Files
dnswatcher/internal/config/config_test.go
T
clawbot fcd4f7e2c2
check / check (push) Successful in 1m3s
config: stop startup on an invalid DNS or TLS interval (closes #177)
DNSWATCHER_DNS_INTERVAL and DNSWATCHER_TLS_INTERVAL were parsed with
time.ParseDuration and silently replaced by the default when that failed,
so a value like 5 or 1d gave hourly checks with no hint why, and zero or
negative values were accepted. Both now go through parseInterval, which
returns an error naming the variable and the value, and startup stops the
same way it does for invalid targets. An unset or empty variable still gets
its default from setupViper. The three tests that pinned the old fallback
are replaced. The README says what a valid value looks like.

Model: opus-5-5
2026-10-01 22:19:04 +02:00

271 lines
7.8 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)
}
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_EmptyIntervalMeansDefault 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_EmptyIntervalMeansDefault(t *testing.T) {
viper.Reset()
t.Setenv("DNSWATCHER_TARGETS", "example.com")
t.Setenv("DNSWATCHER_DNS_INTERVAL", "")
t.Setenv("DNSWATCHER_TLS_INTERVAL", "")
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)
}
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_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())
})
}
}