From b22f8633baeb485c0c5b4dae3a30cc256d18e87d Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 29 Aug 2026 19:19:17 +0800 Subject: [PATCH] autoresearch iter 34: BUGFIX an unreadable SMTP config no longer looks like an unconfigured mailer --- .../domain/message_gateway/repository/push.go | 53 +++++++++++++---- .../message_gateway/repository/push_test.go | 59 +++++++++++++++++++ .../domain/message_gateway/service/push.go | 12 +++- 3 files changed, 110 insertions(+), 14 deletions(-) diff --git a/backend/plugins/domain/message_gateway/repository/push.go b/backend/plugins/domain/message_gateway/repository/push.go index 4e7789f5..a944c3c9 100644 --- a/backend/plugins/domain/message_gateway/repository/push.go +++ b/backend/plugins/domain/message_gateway/repository/push.go @@ -9,6 +9,8 @@ import ( "Wavelet/plugins/domain/message_gateway/errs" "Wavelet/plugins/domain/message_gateway/model" "context" + "errors" + "fmt" "sync" "time" @@ -280,19 +282,46 @@ func PushHistoryQuery(ctx context.Context) *gorm.DB { return GetDB(ctx).Model(&model.PushHistory{}) } -// LoadSMTPConfigRecord reads the SMTP settings owned by the system config table. -func LoadSMTPConfigRecord(ctx context.Context) model.SMTPConfig { +// smtpConfigKeys are the system-config rows backing the built-in email channel. +var smtpConfigKeys = []string{"smtp_host", "smtp_port", "smtp_username", "smtp_password"} + +// LoadSMTPConfigRecord reads the SMTP settings in one query. +// +// A key that is simply absent leaves its field empty, which is how an unconfigured +// mailer is represented. A read that fails is returned as an error, so callers +// cannot mistake an unhealthy database for "no SMTP configured" and silently drop +// the notification. +func LoadSMTPConfigRecord(ctx context.Context) (model.SMTPConfig, error) { + db := GetDB(ctx) + if db == nil { + return model.SMTPConfig{}, errors.New("database not available") + } + + var rows []struct { + Key string + Value string + } + if err := db.Table("w_system_configs"). + Select("key", "value"). + Where("key IN ?", smtpConfigKeys). + Find(&rows).Error; err != nil { + return model.SMTPConfig{}, fmt.Errorf("read smtp system configs: %w", err) + } + var cfg model.SMTPConfig - var host, port, user, pass string - _ = GetDB(ctx).Table("w_system_configs").Where("key = ?", "smtp_host").Pluck("value", &host).Error - _ = GetDB(ctx).Table("w_system_configs").Where("key = ?", "smtp_port").Pluck("value", &port).Error - _ = GetDB(ctx).Table("w_system_configs").Where("key = ?", "smtp_username").Pluck("value", &user).Error - _ = GetDB(ctx).Table("w_system_configs").Where("key = ?", "smtp_password").Pluck("value", &pass).Error - cfg.Host = host - cfg.Port = port - cfg.Username = user - cfg.Password = pass - return cfg + for _, row := range rows { + switch row.Key { + case "smtp_host": + cfg.Host = row.Value + case "smtp_port": + cfg.Port = row.Value + case "smtp_username": + cfg.Username = row.Value + case "smtp_password": + cfg.Password = row.Value + } + } + return cfg, nil } // userLookupColumns allow-lists the columns FindUserByFieldRecord may filter on. diff --git a/backend/plugins/domain/message_gateway/repository/push_test.go b/backend/plugins/domain/message_gateway/repository/push_test.go index 7666ddc6..18376ccd 100644 --- a/backend/plugins/domain/message_gateway/repository/push_test.go +++ b/backend/plugins/domain/message_gateway/repository/push_test.go @@ -11,6 +11,7 @@ import ( "errors" "testing" + "github.com/glebarez/sqlite" "gorm.io/gorm" ) @@ -70,3 +71,61 @@ func TestFindUserByFieldRecordRejectsUnlistedColumns(t *testing.T) { t.Fatalf("w_users damaged by rejected lookups: count=%d err=%v", remaining, err) } } + +// smtpTestValues are the four system-config rows the built-in email channel reads. +var smtpTestValues = map[string]string{ + "smtp_host": "mail.example.test", + "smtp_port": "465", + "smtp_username": "notify@example.test", + "smtp_password": "s3cret-value", +} + +// TestLoadSMTPConfigRecordMapsEveryKey guards the single-query rewrite: every field +// must still be filled from its own row. +func TestLoadSMTPConfigRecordMapsEveryKey(t *testing.T) { + db, _, cleanup := testhelper.SetupTestEnvironment(t) + defer cleanup() + + keys := make([]string, 0, len(smtpTestValues)) + for key := range smtpTestValues { + keys = append(keys, key) + } + if err := db.Table("w_system_configs").Where("key IN ?", keys).Delete(map[string]any{}).Error; err != nil { + t.Fatalf("clear smtp rows: %v", err) + } + for _, key := range keys { + row := map[string]any{"key": key, "value": smtpTestValues[key], "type": "system"} + if err := db.Table("w_system_configs").Create(row).Error; err != nil { + t.Fatalf("seed %s: %v", key, err) + } + } + + repository.SetDBServiceForTest(stubDBService{db: db}) + t.Cleanup(func() { repository.SetDBServiceForTest(nil) }) + + cfg, err := repository.LoadSMTPConfigRecord(context.Background()) + if err != nil { + t.Fatalf("LoadSMTPConfigRecord: %v", err) + } + if cfg.Host != smtpTestValues["smtp_host"] || cfg.Port != smtpTestValues["smtp_port"] || + cfg.Username != smtpTestValues["smtp_username"] || cfg.Password != smtpTestValues["smtp_password"] { + t.Errorf("got %+v, want every SMTP field mapped from its own row", cfg) + } +} + +// TestLoadSMTPConfigRecordSurfacesReadFailure pins the actual defect: a read that +// fails used to be discarded, returning four blank strings that callers could only +// interpret as "SMTP was never configured", so the notification was dropped silently. +func TestLoadSMTPConfigRecordSurfacesReadFailure(t *testing.T) { + bare, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatalf("open bare sqlite: %v", err) + } + + repository.SetDBServiceForTest(stubDBService{db: bare}) + t.Cleanup(func() { repository.SetDBServiceForTest(nil) }) + + if _, err := repository.LoadSMTPConfigRecord(context.Background()); err == nil { + t.Fatal("LoadSMTPConfigRecord returned nil error although the config table cannot be read") + } +} diff --git a/backend/plugins/domain/message_gateway/service/push.go b/backend/plugins/domain/message_gateway/service/push.go index 6f675b21..e45f7bdc 100644 --- a/backend/plugins/domain/message_gateway/service/push.go +++ b/backend/plugins/domain/message_gateway/service/push.go @@ -186,7 +186,11 @@ func ApplySMTPFallbackToPushConfig(ctx context.Context, cfg *pkgpush.Config) { if cfg.Channel != model.ChannelEmail || (cfg.URL != "" && cfg.Key != "") { return } - smtp := repository.LoadSMTPConfigRecord(ctx) + smtp, err := repository.LoadSMTPConfigRecord(ctx) + if err != nil { + logger.ErrorF(ctx, "[Push] 读取 SMTP 系统配置失败: %v", err) + return + } if smtp.Host == "" || smtp.Username == "" { return } @@ -574,7 +578,11 @@ func ResolveSMTPConfig(ctx context.Context, url, token, other string) (string, s if url != "" && token != "" { return url, token, other } - smtp := repository.LoadSMTPConfigRecord(ctx) + smtp, err := repository.LoadSMTPConfigRecord(ctx) + if err != nil { + logger.ErrorF(ctx, "[Push] 读取 SMTP 系统配置失败: %v", err) + return url, token, other + } if smtp.Host == "" || smtp.Username == "" { return url, token, other }