package smtp2shoutrrr import ( "bytes" "log/slog" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" ) // credentials is prepended to fixtures that are not themselves about // authentication, so each one still fails (or passes) for the reason it names // rather than for the credentials it does not mention. const credentials = ` Username = "user" Password = "secret" ` func writeConfig(t *testing.T, contents string) string { t.Helper() path := filepath.Join(t.TempDir(), "config.toml") require.NoError(t, os.WriteFile(path, []byte(contents), 0o600)) return path } func TestLoadConfig(t *testing.T) { path := writeConfig(t, ` Port = 2525 Username = "user" Password = "secret" [[Recipients]] Addresses = ["user@example.com"] Targets = ["ntfy://ntfy.sh/topic"] [[Recipients]] Addresses = ["legacy@example.com"] Target = "ntfy://ntfy.sh/legacy" [CatchAll] Targets = ["ntfy://ntfy.sh/catch-all"] `) config, err := LoadConfig(path) require.NoError(t, err) require.Equal(t, 2525, config.Port) require.Equal(t, "user", config.Username) require.Equal(t, "secret", config.Password) require.Len(t, config.Recipients, 2) require.Equal(t, []string{"user@example.com"}, config.Recipients[0].Addresses) require.Equal(t, []string{"ntfy://ntfy.sh/topic"}, config.Recipients[0].Targets) require.Equal(t, "ntfy://ntfy.sh/legacy", config.Recipients[1].Target) require.NotNil(t, config.CatchAll) require.Equal(t, []string{"ntfy://ntfy.sh/catch-all"}, config.CatchAll.Targets) } func TestLoadConfigAppliesDefaults(t *testing.T) { path := writeConfig(t, credentials+` [[Recipients]] Addresses = ["user@example.com"] Targets = ["ntfy://ntfy.sh/topic"] `) config, err := LoadConfig(path) require.NoError(t, err) require.Equal(t, 11125, config.Port) require.Nil(t, config.CatchAll) // Credentials deliberately have no default; see // TestLoadConfigRejectsUnsetCredentials. require.Equal(t, "user", config.Username) require.Equal(t, "secret", config.Password) } // Regression: unset credentials used to fall back to username/password with // only a warning, so a config naming nothing but a port and its recipients // left the AUTH gate open to the most obvious guess there is — and the README // promised an authentication that had never been configured. func TestLoadConfigRejectsUnsetCredentials(t *testing.T) { recipient := ` [[Recipients]] Addresses = ["user@example.com"] Targets = ["ntfy://ntfy.sh/topic"] ` for name, contents := range map[string]string{ "neither credential": recipient, "no Username": `Password = "secret"` + recipient, "no Password": `Username = "user"` + recipient, "empty Username": `Username = ""` + "\n" + `Password = "secret"` + recipient, "empty Password": `Username = "user"` + "\n" + `Password = ""` + recipient, } { t.Run(name, func(t *testing.T) { config, err := LoadConfig(writeConfig(t, contents)) require.Error(t, err) require.Nil(t, config) }) } } func TestLoadConfigMissingFile(t *testing.T) { _, err := LoadConfig(filepath.Join(t.TempDir(), "does-not-exist.toml")) require.Error(t, err) } func TestLoadConfigInvalidTOML(t *testing.T) { _, err := LoadConfig(writeConfig(t, "Port = ")) require.Error(t, err) } func TestLoadConfigRejectsUnusableConfigurations(t *testing.T) { // A TOML decode ignores unknown keys, so each of these parses cleanly and // would otherwise start a server that forwards nothing. for name, contents := range map[string]string{ "mistyped recipients table": ` [[Recipient]] Addresses = ["user@example.com"] Targets = ["ntfy://ntfy.sh/topic"] `, "mistyped addresses key": ` [[Recipients]] Adresses = ["user@example.com"] Targets = ["ntfy://ntfy.sh/topic"] `, "recipient without targets": ` [[Recipients]] Addresses = ["user@example.com"] `, "recipient with only unparseable targets": ` [[Recipients]] Addresses = ["user@example.com"] Targets = ["://invalid-url"] `, "recipient whose target is not a URL at all": ` [[Recipients]] Addresses = ["user@example.com"] Targets = ["this is not a url at all"] `, "catch-all without targets": ` [CatchAll] Addresses = ["user@example.com"] `, "empty configuration": ` Port = 2525 `, } { t.Run(name, func(t *testing.T) { _, err := LoadConfig(writeConfig(t, credentials+contents)) require.Error(t, err) }) } } func TestLoadConfigAcceptsCatchAllOnly(t *testing.T) { config, err := LoadConfig(writeConfig(t, credentials+` [CatchAll] Targets = ["ntfy://ntfy.sh/catch-all"] `)) require.NoError(t, err) require.Empty(t, config.Recipients) require.NotNil(t, config.CatchAll) } // A mistyped table name is silently dropped by the decoder. It is only fatal // when nothing else is configured, so with a catch-all present the warning is // the sole signal that the block was ignored. func TestLoadConfigWarnsAboutIgnoredKeys(t *testing.T) { var logged bytes.Buffer restore := slog.Default() slog.SetDefault(slog.New(slog.NewTextHandler(&logged, &slog.HandlerOptions{Level: slog.LevelWarn}))) t.Cleanup(func() { slog.SetDefault(restore) }) config, err := LoadConfig(writeConfig(t, credentials+` [[Recipient]] Addresses = ["user@example.com"] Targets = ["ntfy://ntfy.sh/topic"] [CatchAll] Targets = ["ntfy://ntfy.sh/catch-all"] `)) require.NoError(t, err) require.Empty(t, config.Recipients, "the mistyped table is still ignored") require.Contains(t, logged.String(), "ignoring unknown key in config file") require.Contains(t, logged.String(), "Recipient") }