Files
affiliate_dash/backend/internal/service/rock_binding_refresh_test.go
T

707 lines
23 KiB
Go

package service
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"affiliate_dash/internal/model"
"affiliate_dash/internal/pkg/timeutil"
)
func recentRockBindTime(base time.Time) string {
return base.Add(time.Minute).In(timeutil.Location()).Format("2006-01-02 15:04:05")
}
func TestRefreshTokenBinding_BoundMatchesExpectedUID(t *testing.T) {
db := newRockTestDB(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.URL.Path != "/api/redeem/CODE-BOUND/refresh" {
w.WriteHeader(http.StatusNotFound)
return
}
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"token": map[string]interface{}{
"status": "bound",
"bound_account": "痛写心扉",
"bound_account_id": "3132710334",
"bind_changed_at": recentRockBindTime(time.Now()),
"region": "安卓-微信",
},
"binding": map[string]interface{}{
"bound": true,
"role": "痛写心扉",
"roleId": "3132710334",
"region": "安卓-微信",
},
"message": "已刷新绑定信息",
})
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
now := time.Now()
token := model.RockToken{
UpstreamTokenID: "9001",
Token: "jwt-9001",
RedeemCode: "CODE-BOUND",
Color: model.RockColorBlack,
Status: model.RockStatusUsing,
AssignedOrderNo: "FO-REFRESH-1",
ExpectedUID: "3132710334",
ClaimedAt: &now,
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
res, err := svc.RefreshTokenBinding(context.Background(), token.ID)
if err != nil {
t.Fatalf("刷新绑定状态失败: %v", err)
}
if res.Status != model.RockStatusBound || !res.Bound {
t.Fatalf("期望 bound,实际 status=%s bound=%v", res.Status, res.Bound)
}
if res.RoleID != "3132710334" || res.RoleName != "痛写心扉" {
t.Fatalf("角色信息未同步: %+v", res)
}
var saved model.RockToken
if err := db.First(&saved, token.ID).Error; err != nil {
t.Fatalf("读取 Token 失败: %v", err)
}
if saved.Status != model.RockStatusBound || saved.ActualRoleID != "3132710334" || saved.ActualRoleName != "痛写心扉" {
t.Fatalf("数据库未更新: %+v", saved)
}
if saved.BoundAt == nil {
t.Fatalf("bound_at 未写入")
}
}
// 本轮真实绑定与预期 UID 不一致时必须进入 mismatch。
func TestRefreshTokenBinding_UIDMismatchMarksMismatch(t *testing.T) {
db := newRockTestDB(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"token": map[string]interface{}{
"status": "bound",
"bound_account": "痛写心扉",
"bound_account_id": "3132710334",
"bind_changed_at": recentRockBindTime(time.Now()),
"region": "安卓-微信",
},
})
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
now := time.Now()
token := model.RockToken{
UpstreamTokenID: "9002",
RedeemCode: "CODE-MISMATCH",
Color: model.RockColorBlack,
Status: model.RockStatusUsing,
AssignedOrderNo: "FO-REFRESH-2",
ExpectedUID: "1111111111",
ClaimedAt: &now,
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
res, err := svc.RefreshTokenBinding(context.Background(), token.ID)
if err != nil {
t.Fatalf("刷新绑定状态失败: %v", err)
}
if res.Status != model.RockStatusMismatch || !res.Bound {
t.Fatalf("UID 不一致时应同步为 mismatch,实际 %+v", res)
}
var saved model.RockToken
if err := db.First(&saved, token.ID).Error; err != nil {
t.Fatalf("读取 Token 失败: %v", err)
}
if saved.Status != model.RockStatusMismatch || saved.ActualRoleID != "3132710334" {
t.Fatalf("应同步为 mismatch 并写入实际 UID,实际 %+v", saved)
}
if saved.MismatchAt == nil || saved.MismatchReason == "" {
t.Fatalf("UID 不一致必须写入绑定错误标记,实际 %+v", saved)
}
if saved.BoundAt == nil {
t.Fatalf("bound_at 未写入")
}
}
// 本地没有预期 UID 时同样直接按上游状态落库,不再保留 using。
func TestRefreshTokenBinding_EmptyExpectedUIDMarksBound(t *testing.T) {
db := newRockTestDB(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"token": map[string]interface{}{
"bind_changed_at": recentRockBindTime(time.Now()),
},
"binding": map[string]interface{}{
"bound": true,
"role": "痛写心扉",
"roleId": "3132710334",
"region": "安卓-微信",
},
})
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
now := time.Now()
token := model.RockToken{
UpstreamTokenID: "9003",
RedeemCode: "CODE-NOUID",
Color: model.RockColorBlack,
Status: model.RockStatusUsing,
ClaimedAt: &now,
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
res, err := svc.RefreshTokenBinding(context.Background(), token.ID)
if err != nil {
t.Fatalf("刷新绑定状态失败: %v", err)
}
if res.Status != model.RockStatusBound {
t.Fatalf("缺少预期 UID 时也应同步为 bound,实际 %s", res.Status)
}
if !res.Bound {
t.Fatalf("上游已绑定时 bound 应为 true,实际 %+v", res)
}
var saved model.RockToken
if err := db.First(&saved, token.ID).Error; err != nil {
t.Fatalf("读取 Token 失败: %v", err)
}
if saved.Status != model.RockStatusBound || saved.ActualRoleID != "3132710334" || saved.BoundAt == nil {
t.Fatalf("应同步为 bound 并写入角色信息,实际 %+v", saved)
}
}
func TestRefreshTokenBinding_SyncsRedeemed(t *testing.T) {
db := newRockTestDB(t)
now := time.Now()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"token": map[string]interface{}{
"status": "redeemed",
"bound_account": "痛写心扉",
"bound_account_id": "3132710334",
"redeemed_at": now.Add(-20 * time.Second).In(timeutil.Location()).Format("2006-01-02 15:04:05"),
},
"deliveryList": []map[string]interface{}{
{"fulfillId": 4519960, "time": now.Add(-10 * time.Second).Format("2006-01-02T15:04:05.1"), "role": "安卓-微信-痛写心扉"},
},
})
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
token := model.RockToken{
UpstreamTokenID: "9004",
RedeemCode: "CODE-REDEEMED",
Color: model.RockColorBlack,
Status: model.RockStatusUsing,
AssignedOrderNo: "FO-REFRESH-4",
ExpectedUID: "3132710334",
UsingAt: &now,
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
res, err := svc.RefreshTokenBinding(context.Background(), token.ID)
if err != nil {
t.Fatalf("刷新绑定状态失败: %v", err)
}
if res.Status != model.RockStatusRedeemed {
t.Fatalf("期望 redeemed,实际 %s", res.Status)
}
var saved model.RockToken
if err := db.First(&saved, token.ID).Error; err != nil {
t.Fatalf("读取 Token 失败: %v", err)
}
if saved.Status != model.RockStatusRedeemed || saved.RedeemedAt == nil || saved.UpstreamFulfillID != 4519960 {
t.Fatalf("已发货字段未同步: %+v", saved)
}
}
func TestRefreshTokenBinding_IgnoresHistoricalRedeemedAt(t *testing.T) {
db := newRockTestDB(t)
now := time.Now()
historical := now.Add(-48 * time.Hour)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"token": map[string]interface{}{
"status": "redeemed",
"redeemed_at": historical.In(timeutil.Location()).Format("2006-01-02 15:04:05"),
},
"deliveryList": []map[string]interface{}{
{"fulfillId": 100, "time": historical.Format("2006-01-02T15:04:05.1")},
},
})
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
token := model.RockToken{
UpstreamTokenID: "9005",
RedeemCode: "CODE-HISTORICAL-REDEEMED",
Color: model.RockColorBlack,
Status: model.RockStatusUsing,
AssignedOrderNo: "FO-REFRESH-5",
UsingAt: &now,
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
res, err := svc.RefreshTokenBinding(context.Background(), token.ID)
if err != nil {
t.Fatalf("刷新绑定状态失败: %v", err)
}
if res.Status != model.RockStatusUsing || res.Bound {
t.Fatalf("历史兑换记录不能推进本轮状态,实际 %+v", res)
}
var saved model.RockToken
if err := db.First(&saved, token.ID).Error; err != nil {
t.Fatalf("读取 Token 失败: %v", err)
}
if saved.Status != model.RockStatusUsing || saved.RedeemedAt != nil {
t.Fatalf("Token 不应被历史兑换记录改成已发货: %+v", saved)
}
}
func TestRefreshTokenBinding_SnapshotEventAcceptsDifferentTimeFormat(t *testing.T) {
db := newRockTestDB(t)
now := time.Now()
boundAt := now.Add(time.Minute)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"token": map[string]interface{}{
"status": "bound",
"bound_account": "上一任角色",
"bound_account_id": "99001",
"bind_changed_at": boundAt.Format(time.RFC3339Nano),
},
})
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
token := model.RockToken{
UpstreamTokenID: "9006",
RedeemCode: "CODE-SNAPSHOT-FORMAT",
Color: model.RockColorBlack,
Status: model.RockStatusUsing,
AssignedOrderNo: "FO-REFRESH-6",
UsingAt: &now,
InitialBoundSnapshot: `{"bound":true,"role_id":"99001","bind_changed_at":"` + boundAt.In(timeutil.Location()).Format("2006-01-02 15:04:05") + `"}`,
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
res, err := svc.RefreshTokenBinding(context.Background(), token.ID)
if err != nil {
t.Fatalf("刷新绑定状态失败: %v", err)
}
if res.Status != model.RockStatusUsing || res.Bound {
t.Fatalf("与初始快照相同的绑定事件必须按残留处理,实际 %+v", res)
}
}
func TestRefreshTokenBinding_MissingBaselineDoesNotDowngradeBound(t *testing.T) {
db := newRockTestDB(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"token": map[string]interface{}{
"status": "bound",
"bound_account": "历史角色",
"bound_account_id": "88001",
"bind_changed_at": time.Now().In(timeutil.Location()).Add(-time.Hour).Format("2006-01-02 15:04:05"),
},
})
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
token := model.RockToken{
UpstreamTokenID: "9007",
RedeemCode: "CODE-MISSING-BASELINE",
Color: model.RockColorBlack,
Status: model.RockStatusBound,
AssignedOrderNo: "FO-REFRESH-7",
ExpectedUID: "88001",
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
res, err := svc.RefreshTokenBinding(context.Background(), token.ID)
if err != nil {
t.Fatalf("刷新绑定状态失败: %v", err)
}
if res.Status != model.RockStatusBound || !res.Warning {
t.Fatalf("缺少时间基线时只能告警,不能自动降级,实际 %+v", res)
}
var saved model.RockToken
if err := db.First(&saved, token.ID).Error; err != nil {
t.Fatalf("读取 Token 失败: %v", err)
}
if saved.Status != model.RockStatusBound {
t.Fatalf("缺少时间基线时不得自动改写 Token 状态,实际 %s", saved.Status)
}
}
func TestRefreshTokenBinding_UpstreamErrorKeepsState(t *testing.T) {
db := newRockTestDB(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"error":"refresh down"}`))
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
token := model.RockToken{
UpstreamTokenID: "9005",
RedeemCode: "CODE-ERROR",
Color: model.RockColorBlack,
Status: model.RockStatusUsing,
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
if _, err := svc.RefreshTokenBinding(context.Background(), token.ID); err == nil {
t.Fatalf("上游 500 时应返回错误")
}
var saved model.RockToken
if err := db.First(&saved, token.ID).Error; err != nil {
t.Fatalf("读取 Token 失败: %v", err)
}
if saved.Status != model.RockStatusUsing || saved.ActualRoleID != "" {
t.Fatalf("失败时不应改动本地状态: %+v", saved)
}
}
func TestRefreshTokenBinding_FallsBackToAdminRefresh(t *testing.T) {
db := newRockTestDB(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/api/redeem/CODE-FALLBACK/refresh":
w.WriteHeader(http.StatusNotFound)
case "/api/user/tokens/9006/refresh":
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"token": map[string]interface{}{
"status": "bound",
"bound_account": "痛写心扉",
"bound_account_id": "3132710334",
"bind_changed_at": recentRockBindTime(time.Now()),
},
})
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
now := time.Now()
token := model.RockToken{
UpstreamTokenID: "9006",
RedeemCode: "CODE-FALLBACK",
Color: model.RockColorBlack,
Status: model.RockStatusUsing,
AssignedOrderNo: "FO-REFRESH-6",
ExpectedUID: "3132710334",
ClaimedAt: &now,
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
res, err := svc.RefreshTokenBinding(context.Background(), token.ID)
if err != nil {
t.Fatalf("降级刷新失败: %v", err)
}
if res.Status != model.RockStatusBound {
t.Fatalf("降级后期望 bound,实际 %s", res.Status)
}
}
func TestRefreshTokenBinding_NotBoundKeepsRoleFields(t *testing.T) {
db := newRockTestDB(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"token": map[string]interface{}{
"status": "using",
"region": "Q/V可用",
},
"binding": map[string]interface{}{"bound": false},
})
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
token := model.RockToken{
UpstreamTokenID: "9008",
RedeemCode: "CODE-UNBOUND",
Color: model.RockColorBlack,
Status: model.RockStatusUsing,
ActualRoleName: "旧角色",
ActualRoleID: "123456",
ActualChannel: "安卓-微信",
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
res, err := svc.RefreshTokenBinding(context.Background(), token.ID)
if err != nil {
t.Fatalf("刷新绑定状态失败: %v", err)
}
if res.Status != model.RockStatusUsing || res.Bound {
t.Fatalf("上游未绑定时应保持 using 且 bound=false,实际 %+v", res)
}
var saved model.RockToken
if err := db.First(&saved, token.ID).Error; err != nil {
t.Fatalf("读取 Token 失败: %v", err)
}
if saved.ActualRoleName != "旧角色" || saved.ActualRoleID != "123456" || saved.ActualChannel != "安卓-微信" {
t.Fatalf("上游未绑定时不应覆盖本地角色字段: %+v", saved)
}
}
func TestRefreshTokenBinding_RejectsAvailableToken(t *testing.T) {
db := newRockTestDB(t)
svc := NewRockService(db, NewRockClient("https://example.com"))
token := model.RockToken{
UpstreamTokenID: "9007",
RedeemCode: "CODE-AVAILABLE",
Color: model.RockColorBlack,
Status: model.RockStatusAvailable,
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
if _, err := svc.RefreshTokenBinding(context.Background(), token.ID); err == nil || !strings.Contains(err.Error(), "仅扫码使用中") {
t.Fatalf("available 状态应被拒绝,实际 err=%v", err)
}
}
// 上游 status=abnormal 时直接同步为已上报失效,优先级高于残留的绑定信息。
func TestRefreshTokenBinding_UpstreamAbnormalMarksAbnormal(t *testing.T) {
db := newRockTestDB(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"token": map[string]interface{}{
"status": "abnormal",
"bound_account": "痛写心扉",
"bound_account_id": "3132710334",
},
"binding": map[string]interface{}{"bound": true, "roleId": "3132710334"},
})
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
token := model.RockToken{
UpstreamTokenID: "9010",
RedeemCode: "CODE-ABNORMAL",
Status: model.RockStatusUsing,
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
res, err := svc.RefreshTokenBinding(context.Background(), token.ID)
if err != nil {
t.Fatalf("刷新绑定状态失败: %v", err)
}
if res.Status != model.RockStatusAbnormal || !res.Changed {
t.Fatalf("期望 abnormal 且 changed=true,实际 %+v", res)
}
if !strings.Contains(res.Message, "已上报失效") {
t.Fatalf("提示应说明已标记失效,实际 %q", res.Message)
}
var saved model.RockToken
if err := db.First(&saved, token.ID).Error; err != nil {
t.Fatalf("读取 Token 失败: %v", err)
}
if saved.Status != model.RockStatusAbnormal {
t.Fatalf("状态应同步为 abnormal,实际 %s", saved.Status)
}
if saved.Remark != "上游状态异常(刷新自动同步)" {
t.Fatalf("备注应写入系统原因,实际 %q", saved.Remark)
}
}
// 上游明确返回 Token 不存在(公开端与管理端都 404)时,绑定错误 Token 也同步为已上报失效。
func TestRefreshTokenBinding_UpstreamDeletedMarksAbnormal(t *testing.T) {
db := newRockTestDB(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"error":"Token 不存在"}`))
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
now := time.Now()
token := model.RockToken{
UpstreamTokenID: "9011",
RedeemCode: "CODE-DELETED",
Status: model.RockStatusMismatch,
ExpectedUID: "1755431394",
MismatchAt: &now,
MismatchReason: "预期UID(1755431394) 与 扫码UID(Nfestx/1920708170) 不一致",
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
res, err := svc.RefreshTokenBinding(context.Background(), token.ID)
if err != nil {
t.Fatalf("刷新绑定状态失败: %v", err)
}
if res.Status != model.RockStatusAbnormal {
t.Fatalf("上游删除时应同步 abnormal,实际 %s", res.Status)
}
var saved model.RockToken
if err := db.First(&saved, token.ID).Error; err != nil {
t.Fatalf("读取 Token 失败: %v", err)
}
if saved.Status != model.RockStatusAbnormal {
t.Fatalf("状态应同步为 abnormal,实际 %s", saved.Status)
}
if saved.Remark != "上游卡密不存在(刷新自动同步)" {
t.Fatalf("备注应写入系统原因,实际 %q", saved.Remark)
}
if saved.MismatchAt == nil {
t.Fatalf("绑定错误历史应保留")
}
}
// 普通 404(网关/路由)不能被误判为卡密删除。
func TestRefreshTokenBinding_Generic404KeepsState(t *testing.T) {
db := newRockTestDB(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"error":"404 page not found"}`))
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
token := model.RockToken{
UpstreamTokenID: "9012",
RedeemCode: "CODE-GATEWAY-404",
Status: model.RockStatusUsing,
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
if _, err := svc.RefreshTokenBinding(context.Background(), token.ID); err == nil {
t.Fatalf("普通 404 应返回错误,不能标记失效")
}
var saved model.RockToken
if err := db.First(&saved, token.ID).Error; err != nil {
t.Fatalf("读取 Token 失败: %v", err)
}
if saved.Status != model.RockStatusUsing || saved.Remark != "" {
t.Fatalf("普通 404 不应改动状态或备注,实际 %+v", saved)
}
}
// 已有备注时追加系统原因,不覆盖人工内容。
func TestRefreshTokenBinding_AbnormalAppendsManualRemark(t *testing.T) {
db := newRockTestDB(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"token": map[string]interface{}{"status": "abnormal"},
})
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
token := model.RockToken{
UpstreamTokenID: "9013",
RedeemCode: "CODE-ABNORMAL-REMARK",
Status: model.RockStatusBound,
Remark: "客服人工备注",
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
if _, err := svc.RefreshTokenBinding(context.Background(), token.ID); err != nil {
t.Fatalf("刷新绑定状态失败: %v", err)
}
var saved model.RockToken
if err := db.First(&saved, token.ID).Error; err != nil {
t.Fatalf("读取 Token 失败: %v", err)
}
if saved.Status != model.RockStatusAbnormal {
t.Fatalf("应标记 abnormal,实际 %s", saved.Status)
}
wantRemark := "客服人工备注\n上游状态异常(刷新自动同步)"
if saved.Remark != wantRemark {
t.Fatalf("应追加系统原因并保留人工备注,实际 %q", saved.Remark)
}
}