package service import ( "strings" "testing" "affiliate_dash/internal/model" ) func TestMaskAlertSecret(t *testing.T) { cases := []struct { name string input string want string }{ {"空值原样返回", "", ""}, {"纯空白原样返回", " ", ""}, {"长度不足 4 位全部遮蔽", "abcd", alertSecretMask}, {"正好 5 位保留末 4 位", "abcde", alertSecretMask + "bcde"}, {"长密钥保留末 4 位", "access_token_value_1234", alertSecretMask + "1234"}, {"前后空白被裁剪", " secret1234 ", alertSecretMask + "1234"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := MaskAlertSecret(tc.input) if got != tc.want { t.Fatalf("期望 %q,实际 %q", tc.want, got) } }) } } func TestIsMaskedAlertSecret(t *testing.T) { if !IsMaskedAlertSecret(alertSecretMask + "1234") { t.Fatal("整体掩码(Bark Key)应被识别") } if !IsMaskedAlertSecret("https://oapi.dingtalk.com/robot/send?access_token=" + alertSecretMask + "1234") { t.Fatal("嵌在 URL 中间的掩码应被识别") } if IsMaskedAlertSecret("real-secret-1234") { t.Fatal("真实密钥不应被误判为脱敏值") } if IsMaskedAlertSecret("") { t.Fatal("空值不应被当作脱敏值") } } // TestMaskAlertWebhookURL 覆盖三种真实机器人的 URL 形态: // 钉钉/企微把 token 放在查询串,飞书放在路径末段。 func TestMaskAlertWebhookURL(t *testing.T) { cases := []struct { name string input string want string }{ { name: "钉钉查询串保留域名与路径", input: "https://oapi.dingtalk.com/robot/send?access_token=abcdef123456", want: "https://oapi.dingtalk.com/robot/send?access_token=" + alertSecretMask + "3456", }, { name: "企业微信查询串", input: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=aaaa-bbbb-cccc-dddd", want: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" + alertSecretMask + "dddd", }, { name: "飞书路径末段 uuid 被遮蔽", input: "https://open.feishu.cn/open-apis/bot/v2/hook/11111111-2222-3333-4444-555555555555", want: "https://open.feishu.cn/open-apis/bot/v2/hook/" + alertSecretMask + "5555", }, { name: "短路径末段不误伤(钉钉 send)", input: "https://oapi.dingtalk.com/robot/send", want: "https://oapi.dingtalk.com/robot/send", }, { name: "空值原样返回", input: "", want: "", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := maskAlertWebhookURL(tc.input) if got != tc.want { t.Fatalf("期望 %q,实际 %q", tc.want, got) } if strings.Contains(got, "%2A") { t.Fatalf("掩码里的 * 不应被百分号转义:%q", got) } }) } } // TestMaskAlertWebhookURLKeepsURLValid 脱敏结果必须仍是合法 http(s) URL, // 否则前端原样回传时会撞上 ValidateAlertChannelInput。 func TestMaskAlertWebhookURLKeepsURLValid(t *testing.T) { urls := []string{ "https://oapi.dingtalk.com/robot/send?access_token=abcdef123456", "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=aaaa-bbbb-cccc-dddd", "https://open.feishu.cn/open-apis/bot/v2/hook/11111111-2222-3333-4444-555555555555", } for _, raw := range urls { masked := maskAlertWebhookURL(raw) if err := ValidateAlertChannelInput(AlertChannelInput{ ChannelType: model.AlertChannelDingtalk, Config: model.AlertChannelConfig{WebhookURL: masked}, }); err != nil { t.Fatalf("脱敏后的 URL 未通过校验:%q -> %q (%v)", raw, masked, err) } } } func TestMaskAlertChannelKeepsServer(t *testing.T) { channel := model.AlertNotifyChannel{ ChannelType: model.AlertChannelBark, Name: "老板手机", Config: model.AlertChannelConfig{ Server: "https://api.day.app", Key: "device-key-9876", }, } masked := MaskAlertChannel(channel) if masked.Config.Server != "https://api.day.app" { t.Fatalf("Server 是主机地址而非密钥,不应被遮蔽,实际 %q", masked.Config.Server) } if masked.Config.Key != alertSecretMask+"9876" { t.Fatalf("Key 应被遮蔽,实际 %q", masked.Config.Key) } if channel.Config.Key != "device-key-9876" { t.Fatal("MaskAlertChannel 不应修改入参") } } func TestMaskAlertChannelsNil(t *testing.T) { if MaskAlertChannels(nil) != nil { t.Fatal("nil 输入应返回 nil,便于直接替换响应体") } } // TestUpdateAlertChannelWithMaskedSecret 验证脱敏值回传时不会把真实密钥覆盖成掩码。 func TestUpdateAlertChannelWithMaskedSecret(t *testing.T) { db := newServiceTestDB(t) merchantID, _ := seedFulfillmentMerchant(t, db, "merchant-alert-mask", 0, -1, 100) svc := NewRechargeService(db, NewFulfillmentService(db, nil), t.TempDir()) const realWebhook = "https://oapi.dingtalk.com/robot/send?access_token=realtoken1234" created, err := svc.CreateAlertChannel(merchantID, AlertChannelInput{ ChannelType: model.AlertChannelDingtalk, Name: "财务群", Enabled: true, Config: model.AlertChannelConfig{WebhookURL: realWebhook}, }) if err != nil { t.Fatalf("创建渠道失败: %v", err) } // 模拟前端:拿到脱敏值后只改名称,原样回传 webhook。 maskedForm := MaskAlertChannel(*created) if maskedForm.Config.WebhookURL == realWebhook { t.Fatal("响应中的 webhook 应已脱敏") } updated, err := svc.UpdateAlertChannel(merchantID, created.ID, AlertChannelInput{ ChannelType: maskedForm.ChannelType, Name: "财务群(改名)", Enabled: false, Config: maskedForm.Config, }) if err != nil { t.Fatalf("以脱敏值更新应被接受: %v", err) } if updated.Name != "财务群(改名)" || updated.Enabled { t.Fatalf("名称/开关应被更新,实际 %+v", updated) } if updated.Config.WebhookURL != realWebhook { t.Fatalf("真实密钥被掩码覆盖了:%q", updated.Config.WebhookURL) } // 重新读取,确认落库值仍是真实密钥(否则告警会发不出去)。 var stored model.AlertNotifyChannel if err := db.First(&stored, created.ID).Error; err != nil { t.Fatalf("读取渠道失败: %v", err) } if stored.Config.WebhookURL != realWebhook { t.Fatalf("库中密钥被掩码覆盖:%q", stored.Config.WebhookURL) } // 用户主动填写新密钥时,必须真正替换。 const newWebhook = "https://oapi.dingtalk.com/robot/send?access_token=newtoken5678" replaced, err := svc.UpdateAlertChannel(merchantID, created.ID, AlertChannelInput{ ChannelType: model.AlertChannelDingtalk, Name: "财务群(改名)", Enabled: true, Config: model.AlertChannelConfig{WebhookURL: newWebhook}, }) if err != nil { t.Fatalf("更新新密钥失败: %v", err) } if replaced.Config.WebhookURL != newWebhook { t.Fatalf("新密钥未被写入:%q", replaced.Config.WebhookURL) } } // TestAlertChannelsServiceStillReturnsRawSecret 卡住一个关键边界: // service 层不能脱敏,否则余额巡检拿不到真实 webhook 就发不出告警。 func TestAlertChannelsServiceStillReturnsRawSecret(t *testing.T) { db := newServiceTestDB(t) merchantID, _ := seedFulfillmentMerchant(t, db, "merchant-alert-raw", 0, -1, 100) svc := NewRechargeService(db, NewFulfillmentService(db, nil), t.TempDir()) const realWebhook = "https://oapi.dingtalk.com/robot/send?access_token=rawtoken9999" if _, err := svc.CreateAlertChannel(merchantID, AlertChannelInput{ ChannelType: model.AlertChannelDingtalk, Name: "财务群", Enabled: true, Config: model.AlertChannelConfig{WebhookURL: realWebhook}, }); err != nil { t.Fatalf("创建渠道失败: %v", err) } list, err := svc.ListAlertChannels(merchantID) if err != nil || len(list) != 1 { t.Fatalf("期望 1 个渠道,实际 %d,err=%v", len(list), err) } if list[0].Config.WebhookURL != realWebhook { t.Fatalf("service 层必须返回真实密钥(供发送告警使用),实际 %q", list[0].Config.WebhookURL) } }