- 回调订阅改为单记录 upsert:保存即覆盖(URL/事件/状态),自动停用其他订阅,无新增/删除 - 新增重置密钥:保存时传 rotate_secret=true 重新生成密钥并返回一次,解决密钥丢失无法找回 - 删除不再使用的 PATCH /callbacks/:id/status 路由及对应 handler/service 死代码 - 前端回调页改为内联表单(URL/事件/状态),新增重置密钥按钮(确认后展示新密钥一次) - 补充单地址复用、disabled 不投递、密钥轮换测试
146 lines
4.6 KiB
Go
146 lines
4.6 KiB
Go
package service
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"affiliate_dash/internal/model"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func TestCallbackSubscriptionSaveReusesSingleConfigAndControlsDelivery(t *testing.T) {
|
|
db := newServiceTestDB(t)
|
|
merchantID, _ := seedFulfillmentMerchant(t, db, "callback-single-config", 1000, 1, 100)
|
|
codec, err := NewSecretCodec("test-master-key")
|
|
if err != nil {
|
|
t.Fatalf("codec: %v", err)
|
|
}
|
|
svc := NewCallbackService(db, codec)
|
|
|
|
first, err := svc.CreateSubscription(merchantID, CreateCallbackInput{
|
|
URL: "https://example.com/old-callback",
|
|
Events: "order.shipping.updated",
|
|
Status: model.CallbackStatusActive,
|
|
}, 7)
|
|
if err != nil {
|
|
t.Fatalf("create callback: %v", err)
|
|
}
|
|
if first.Secret == "" {
|
|
t.Fatalf("new callback should return secret once")
|
|
}
|
|
|
|
second, err := svc.CreateSubscription(merchantID, CreateCallbackInput{
|
|
URL: "https://example.com/new-callback",
|
|
Events: "order.shipping.updated",
|
|
Status: model.CallbackStatusDisabled,
|
|
}, 7)
|
|
if err != nil {
|
|
t.Fatalf("update callback: %v", err)
|
|
}
|
|
if second.Subscription.ID != first.Subscription.ID {
|
|
t.Fatalf("callback should reuse one row, first=%d second=%d", first.Subscription.ID, second.Subscription.ID)
|
|
}
|
|
if second.Secret != "" {
|
|
t.Fatalf("updated callback should not rotate or reveal secret")
|
|
}
|
|
|
|
var subscriptionCount int64
|
|
if err := db.Model(&model.CallbackSubscription{}).Where("merchant_id = ?", merchantID).Count(&subscriptionCount).Error; err != nil {
|
|
t.Fatalf("count subscriptions: %v", err)
|
|
}
|
|
if subscriptionCount != 1 {
|
|
t.Fatalf("expected one callback subscription, got %d", subscriptionCount)
|
|
}
|
|
|
|
if err := db.Transaction(func(tx *gorm.DB) error {
|
|
return svc.Enqueue(tx, merchantID, "order.shipping.updated", map[string]string{"source": "test"})
|
|
}); err != nil {
|
|
t.Fatalf("enqueue disabled callback: %v", err)
|
|
}
|
|
var deliveryCount int64
|
|
if err := db.Model(&model.CallbackDelivery{}).Where("merchant_id = ?", merchantID).Count(&deliveryCount).Error; err != nil {
|
|
t.Fatalf("count deliveries: %v", err)
|
|
}
|
|
if deliveryCount != 0 {
|
|
t.Fatalf("disabled callback should not enqueue delivery, got %d", deliveryCount)
|
|
}
|
|
|
|
third, err := svc.CreateSubscription(merchantID, CreateCallbackInput{
|
|
URL: "https://example.com/new-callback",
|
|
Events: "order.shipping.updated",
|
|
Status: model.CallbackStatusActive,
|
|
}, 7)
|
|
if err != nil {
|
|
t.Fatalf("enable callback: %v", err)
|
|
}
|
|
if third.Subscription.ID != first.Subscription.ID {
|
|
t.Fatalf("enabled callback should still reuse one row")
|
|
}
|
|
if err := db.Transaction(func(tx *gorm.DB) error {
|
|
return svc.Enqueue(tx, merchantID, "order.shipping.updated", map[string]string{"source": "test"})
|
|
}); err != nil {
|
|
t.Fatalf("enqueue active callback: %v", err)
|
|
}
|
|
if err := db.Model(&model.CallbackDelivery{}).Where("merchant_id = ?", merchantID).Count(&deliveryCount).Error; err != nil {
|
|
t.Fatalf("count deliveries after enable: %v", err)
|
|
}
|
|
if deliveryCount != 1 {
|
|
t.Fatalf("active callback should enqueue one delivery, got %d", deliveryCount)
|
|
}
|
|
}
|
|
|
|
func TestCallbackSubscriptionRotateSecret(t *testing.T) {
|
|
db := newServiceTestDB(t)
|
|
merchantID, _ := seedFulfillmentMerchant(t, db, "callback-rotate-secret", 1000, 1, 100)
|
|
codec, err := NewSecretCodec("test-master-key")
|
|
if err != nil {
|
|
t.Fatalf("codec: %v", err)
|
|
}
|
|
svc := NewCallbackService(db, codec)
|
|
|
|
first, err := svc.CreateSubscription(merchantID, CreateCallbackInput{
|
|
URL: "https://example.com/cb",
|
|
Events: "order.shipping.updated",
|
|
Status: model.CallbackStatusActive,
|
|
}, 7)
|
|
if err != nil {
|
|
t.Fatalf("create callback: %v", err)
|
|
}
|
|
if first.Secret == "" {
|
|
t.Fatalf("new callback should return secret")
|
|
}
|
|
|
|
// 普通更新不轮换密钥
|
|
updated, err := svc.CreateSubscription(merchantID, CreateCallbackInput{
|
|
URL: "https://example.com/cb",
|
|
Events: "order.shipping.updated",
|
|
Status: model.CallbackStatusActive,
|
|
}, 7)
|
|
if err != nil {
|
|
t.Fatalf("update callback: %v", err)
|
|
}
|
|
if updated.Secret != "" {
|
|
t.Fatalf("plain update should not rotate secret")
|
|
}
|
|
|
|
// 显式重置密钥:返回新 secret,且与旧 secret 不同
|
|
rotated, err := svc.CreateSubscription(merchantID, CreateCallbackInput{
|
|
URL: "https://example.com/cb",
|
|
Events: "order.shipping.updated",
|
|
Status: model.CallbackStatusActive,
|
|
RotateSecret: true,
|
|
}, 7)
|
|
if err != nil {
|
|
t.Fatalf("rotate callback: %v", err)
|
|
}
|
|
if rotated.Secret == "" {
|
|
t.Fatalf("rotate should return new secret")
|
|
}
|
|
if rotated.Secret == first.Secret {
|
|
t.Fatalf("rotated secret should differ from old secret")
|
|
}
|
|
if rotated.Subscription.ID != first.Subscription.ID {
|
|
t.Fatalf("rotate should keep same subscription row")
|
|
}
|
|
}
|