package config_test import ( "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, "./data", 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) } func TestNew_InvalidDNSInterval_FallsBackToDefault(t *testing.T) { viper.Reset() t.Setenv("DNSWATCHER_TARGETS", "example.com") t.Setenv("DNSWATCHER_DNS_INTERVAL", "banana") cfg, err := config.New(nil, newTestParams(t)) require.NoError(t, err) assert.Equal(t, time.Hour, cfg.DNSInterval, "invalid DNS interval should fall back to 1h default") } func TestNew_InvalidTLSInterval_FallsBackToDefault(t *testing.T) { viper.Reset() t.Setenv("DNSWATCHER_TARGETS", "example.com") t.Setenv("DNSWATCHER_TLS_INTERVAL", "notaduration") cfg, err := config.New(nil, newTestParams(t)) require.NoError(t, err) assert.Equal(t, 12*time.Hour, cfg.TLSInterval, "invalid TLS interval should fall back to 12h default") } func TestNew_BothIntervalsInvalid(t *testing.T) { viper.Reset() t.Setenv("DNSWATCHER_TARGETS", "example.com") t.Setenv("DNSWATCHER_DNS_INTERVAL", "xyz") t.Setenv("DNSWATCHER_TLS_INTERVAL", "abc") 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", "./data", "./data/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()) }) } }