package distribution import ( "context" "testing" "hfb_sys/backend/internal/model" smsnode "hfb_sys/backend/internal/modules/sms" "go.uber.org/zap" ) func TestParseSiteSMSConfigNormalizesBusinessTemplates(t *testing.T) { cfg, err := ParseSiteSMSConfig(`{ "enabled": true, "sign_name": "分站签名", "business_templates": { "order_handoff_assist": " SMS_512540945 ", "listing_review_rejected": " ", "unknown_node": "SMS_BAD", "": "SMS_EMPTY" } }`) if err != nil { t.Fatalf("ParseSiteSMSConfig() error = %v", err) } if len(cfg.BusinessTemplates) != 1 { t.Fatalf("business_templates = %+v, want 仅保留已登记且非空的模板", cfg.BusinessTemplates) } if got := cfg.BusinessTemplates[smsnode.NodeOrderHandoffAssist]; got != "SMS_512540945" { t.Fatalf("order_handoff_assist = %q, want SMS_512540945", got) } if _, ok := cfg.BusinessTemplates["unknown_node"]; ok { t.Fatal("未登记节点不应保留") } } func TestUpdateSiteSMSConfigPersistsBusinessTemplates(t *testing.T) { db := distributionTestDB(t) repo := NewRepository(db, nil) ctx := context.Background() site := model.DistributionSite{ SiteCode: "site_sms_node", SiteName: "节点短信站", Domain: "smsnode.com", Status: "active", } if err := db.Create(&site).Error; err != nil { t.Fatal(err) } templates := map[string]string{ smsnode.NodeOrderHandoffAssist: "SMS_512540945", smsnode.NodeListingReviewRejected: "SMS_512520993", "unknown_node": "SMS_BAD", } updated, err := repo.UpdateSiteSMSConfig(ctx, 0, site.ID, UpdateSiteSMSConfigRequest{ BusinessTemplates: &templates, }) if err != nil { t.Fatalf("UpdateSiteSMSConfig() error = %v", err) } if len(updated.BusinessTemplates) != 2 { t.Fatalf("business_templates = %+v, want 2 项(过滤未登记节点)", updated.BusinessTemplates) } // 未传 BusinessTemplates 时不能被清空。 newSign := "分站签名" if _, err := repo.UpdateSiteSMSConfig(ctx, 0, site.ID, UpdateSiteSMSConfigRequest{SignName: &newSign}); err != nil { t.Fatalf("UpdateSiteSMSConfig() error = %v", err) } reloaded, err := repo.GetSiteSMSConfig(ctx, 0, site.ID) if err != nil { t.Fatalf("GetSiteSMSConfig() error = %v", err) } if reloaded.BusinessTemplates[smsnode.NodeOrderHandoffAssist] != "SMS_512540945" { t.Fatalf("未传该字段时模板应保留,实际 = %+v", reloaded.BusinessTemplates) } // 传空对象表示清空,节点回退主站模板。 empty := map[string]string{} cleared, err := repo.UpdateSiteSMSConfig(ctx, 0, site.ID, UpdateSiteSMSConfigRequest{BusinessTemplates: &empty}) if err != nil { t.Fatalf("UpdateSiteSMSConfig() error = %v", err) } if len(cleared.BusinessTemplates) != 0 { t.Fatalf("business_templates 应被清空, 实际 = %+v", cleared.BusinessTemplates) } } // 后台改了站点短信凭据后,长期运行的任务必须能自动重建 Provider, // 否则更换 AccessKey 要重启进程才会生效。 func TestResolveBusinessSenderRebuildsProviderAfterCredentialChange(t *testing.T) { db := distributionTestDB(t) repo := NewRepository(db, nil) repo.SetAllowMockSMS(true) ctx := context.Background() site := model.DistributionSite{ SiteCode: "site_sms_rotate", SiteName: "凭据轮换站", Domain: "smsrotate.com", Status: "active", } if err := db.Create(&site).Error; err != nil { t.Fatal(err) } enabled := true provider := "mock" sign := "【分站签名】" oldKey := "LTAI_OLD_KEY" if _, err := repo.UpdateSiteSMSConfig(ctx, 0, site.ID, UpdateSiteSMSConfigRequest{ Enabled: &enabled, Provider: &provider, SignName: &sign, AccessKeyID: &oldKey, }); err != nil { t.Fatalf("UpdateSiteSMSConfig() error = %v", err) } resolver := NewSMSResolver(repo, zap.NewNop()) first, _, _, err := resolver.ResolveBusinessSender(ctx, site.ID) if err != nil { t.Fatalf("ResolveBusinessSender() error = %v", err) } if first == nil { t.Fatal("站点已启用独立短信时应返回通道") } // 凭据未变化时应命中缓存,返回同一实例。 again, _, _, err := resolver.ResolveBusinessSender(ctx, site.ID) if err != nil { t.Fatalf("ResolveBusinessSender() error = %v", err) } if again != first { t.Fatal("凭据未变化时应复用缓存 Provider") } newKey := "LTAI_NEW_KEY" if _, err := repo.UpdateSiteSMSConfig(ctx, 0, site.ID, UpdateSiteSMSConfigRequest{ AccessKeyID: &newKey, }); err != nil { t.Fatalf("UpdateSiteSMSConfig() error = %v", err) } rebuilt, _, _, err := resolver.ResolveBusinessSender(ctx, site.ID) if err != nil { t.Fatalf("ResolveBusinessSender() error = %v", err) } if rebuilt == nil { t.Fatal("凭据轮换后仍应返回可用通道") } if rebuilt == first { t.Fatal("凭据轮换后应重建 Provider,而不是继续复用旧缓存") } } func TestResolveBusinessSenderReturnsSiteTemplates(t *testing.T) { db := distributionTestDB(t) repo := NewRepository(db, nil) repo.SetAllowMockSMS(true) ctx := context.Background() site := model.DistributionSite{ SiteCode: "site_sms_sender", SiteName: "节点短信通道站", Domain: "smssender.com", Status: "active", } if err := db.Create(&site).Error; err != nil { t.Fatal(err) } enabled := true provider := "mock" sign := "【分站签名】" templates := map[string]string{smsnode.NodeOrderHandoffAssist: "SMS_SITE"} if _, err := repo.UpdateSiteSMSConfig(ctx, 0, site.ID, UpdateSiteSMSConfigRequest{ Enabled: &enabled, Provider: &provider, SignName: &sign, BusinessTemplates: &templates, }); err != nil { t.Fatalf("UpdateSiteSMSConfig() error = %v", err) } resolver := NewSMSResolver(repo, zap.NewNop()) sender, signName, siteTemplates, err := resolver.ResolveBusinessSender(ctx, site.ID) if err != nil { t.Fatalf("ResolveBusinessSender() error = %v", err) } if sender == nil { t.Fatal("站点已启用独立短信时应返回通道") } if signName != sign { t.Fatalf("signName = %q, want %q", signName, sign) } if siteTemplates[smsnode.NodeOrderHandoffAssist] != "SMS_SITE" { t.Fatalf("站点模板 = %+v", siteTemplates) } // 站点未启用独立短信时返回 nil,调用方回退主站通道。 other := model.DistributionSite{ SiteCode: "site_sms_off", SiteName: "未启用短信站", Domain: "smsoff.com", Status: "active", } if err := db.Create(&other).Error; err != nil { t.Fatal(err) } disabledSender, _, _, err := resolver.ResolveBusinessSender(ctx, other.ID) if err != nil { t.Fatalf("ResolveBusinessSender() error = %v", err) } if disabledSender != nil { t.Fatal("站点未启用时应返回 nil 以便回退主站") } // 站点不存在(或非 active)时同样返回 nil,不报错。 missingSender, _, _, err := resolver.ResolveBusinessSender(ctx, 999999) if err != nil { t.Fatalf("ResolveBusinessSender() error = %v", err) } if missingSender != nil { t.Fatal("站点不存在时应返回 nil") } }