fix: require DSNs for legacy database notifications

Reject pre-KV PostgreSQL and MySQL targets that lack a canonical connection string, propagate the typed migration error to the fatal startup boundary, and stop emitting unregistered discrete connection keys.\n\nCloses the implementation for #53; release and issue closure remain separate gates.
This commit is contained in:
Feng Ruohang
2026-08-24 02:22:20 +08:00
parent 43f4bb7ed4
commit f1ba683582
6 changed files with 525 additions and 62 deletions
+190
View File
@@ -18,6 +18,8 @@
package notify
import (
"errors"
"strings"
"testing"
"github.com/minio/minio/internal/config"
@@ -26,6 +28,25 @@ import (
"github.com/rabbitmq/amqp091-go"
)
func assertLegacyDatabaseTargetError(t *testing.T, err error, subsystem, name, key string, secrets ...string) {
t.Helper()
var targetErr *LegacyDatabaseTargetError
if !errors.As(err, &targetErr) {
t.Fatalf("error = %v, want *LegacyDatabaseTargetError", err)
}
msg := err.Error()
for _, want := range []string{subsystem + config.SubSystemSeparator + name, key} {
if !strings.Contains(msg, want) {
t.Errorf("error %q does not contain %q", msg, want)
}
}
for _, secret := range secrets {
if secret != "" && strings.Contains(msg, secret) {
t.Errorf("error leaks configuration value %q: %s", secret, msg)
}
}
}
// T5 (NATS): a config produced by the legacy migration must survive validation
// and round-trip back through the parser unchanged. Before the fix the
// migration wrote an env var name as a config key, so every migrated NATS
@@ -113,3 +134,172 @@ func TestSetNotifyAMQPRoundTrip(t *testing.T) {
t.Errorf("Internal = true, want false (immediate must not be written to the internal key)")
}
}
func TestSetNotifyDatabaseTargetsRequireConnectionStrings(t *testing.T) {
postgresHost, err := xnet.ParseHost("legacy-postgres.example")
if err != nil {
t.Fatal(err)
}
mysqlHost, err := xnet.ParseURL("legacy-mysql.example")
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
subsystem string
key string
set func(config.Config) error
secrets []string
}{
{
name: "postgres",
subsystem: config.NotifyPostgresSubSys,
key: target.PostgresConnectionString,
set: func(s config.Config) error {
return SetNotifyPostgres(s, testTargetName, target.PostgreSQLArgs{
Enable: true,
Format: formatNamespace,
Table: "events",
Host: *postgresHost,
Port: "5432",
Username: "legacy-user",
Password: "legacy-postgres-password",
Database: "legacy-database",
})
},
secrets: []string{postgresHost.String(), "5432", "legacy-user", "legacy-postgres-password", "legacy-database"},
},
{
name: "mysql",
subsystem: config.NotifyMySQLSubSys,
key: target.MySQLDSNString,
set: func(s config.Config) error {
return SetNotifyMySQL(s, testTargetName, target.MySQLArgs{
Enable: true,
Format: formatNamespace,
Table: "events",
Host: *mysqlHost,
Port: "3306",
User: "legacy-user",
Password: "legacy-mysql-password",
Database: "legacy-database",
})
},
secrets: []string{mysqlHost.String(), "3306", "legacy-user", "legacy-mysql-password", "legacy-database"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := config.Config{test.subsystem: map[string]config.KVS{}}
err := test.set(s)
assertLegacyDatabaseTargetError(t, err, test.subsystem, testTargetName, test.key, test.secrets...)
if _, ok := s[test.subsystem][testTargetName]; ok {
t.Fatal("unsupported target was emitted despite migration error")
}
})
}
}
func TestSetNotifyDisabledDatabaseTargetsAreIgnored(t *testing.T) {
s := config.Config{
config.NotifyPostgresSubSys: map[string]config.KVS{},
config.NotifyMySQLSubSys: map[string]config.KVS{},
}
if err := SetNotifyPostgres(s, testTargetName, target.PostgreSQLArgs{Password: "discarded-postgres-secret"}); err != nil {
t.Fatalf("SetNotifyPostgres: %v", err)
}
if err := SetNotifyMySQL(s, testTargetName, target.MySQLArgs{Password: "discarded-mysql-secret"}); err != nil {
t.Fatalf("SetNotifyMySQL: %v", err)
}
if _, ok := s[config.NotifyPostgresSubSys][testTargetName]; ok {
t.Fatal("disabled Postgres target was emitted")
}
if _, ok := s[config.NotifyMySQLSubSys][testTargetName]; ok {
t.Fatal("disabled MySQL target was emitted")
}
}
func TestSetNotifyInvalidDatabaseTargetsDoNotLeak(t *testing.T) {
tests := []struct {
name string
subsystem string
key string
secret string
set func(config.Config) error
}{
{
name: "postgres",
subsystem: config.NotifyPostgresSubSys,
key: target.PostgresConnectionString,
secret: "postgres-dsn-secret",
set: func(s config.Config) error {
return SetNotifyPostgres(s, testTargetName, target.PostgreSQLArgs{
Enable: true,
Format: formatNamespace,
ConnectionString: "host=db password=postgres-dsn-secret",
})
},
},
{
name: "mysql",
subsystem: config.NotifyMySQLSubSys,
key: target.MySQLDSNString,
secret: "mysql-dsn-secret",
set: func(s config.Config) error {
return SetNotifyMySQL(s, testTargetName, target.MySQLArgs{
Enable: true,
Format: formatNamespace,
DSN: "user:mysql-dsn-secret@tcp(db:3306/events",
Table: "events",
})
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := config.Config{test.subsystem: map[string]config.KVS{}}
err := test.set(s)
assertLegacyDatabaseTargetError(t, err, test.subsystem, testTargetName, test.key, test.secret)
})
}
}
func TestDatabaseConnectionStringsSurviveKVTokenization(t *testing.T) {
tests := []struct {
name string
subsystem string
key string
input string
want string
}{
{
name: "postgres",
subsystem: config.NotifyPostgresSubSys,
key: target.PostgresConnectionString,
input: `notify_postgres:dsn connection_string="host=db port=5432 dbname=events user=app password=inside" table="events"`,
want: "host=db port=5432 dbname=events user=app password=inside",
},
{
name: "mysql",
subsystem: config.NotifyMySQLSubSys,
key: target.MySQLDSNString,
input: `notify_mysql:dsn dsn_string="user:pass@tcp(db:3306)/events?host=db&port=3306&password=inside" table="events"`,
want: "user:pass@tcp(db:3306)/events?host=db&port=3306&password=inside",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := config.Config{test.subsystem: map[string]config.KVS{}}
if _, err := s.SetKVS(test.input, DefaultNotificationKVS); err != nil {
t.Fatalf("SetKVS: %v", err)
}
if got := s[test.subsystem]["dsn"].Get(test.key); got != test.want {
t.Errorf("%s = %q, want %q", test.key, got, test.want)
}
})
}
}