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

554 lines
18 KiB
Go

package service
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"affiliate_dash/internal/model"
)
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",
"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 与扫码 UID 不一致,也应直接标记为已绑定。
func TestRefreshTokenBinding_IgnoresUIDMismatchAndMarksBound(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",
"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.RockStatusBound || !res.Bound {
t.Fatalf("UID 不一致时也应同步为 bound,实际 %+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" {
t.Fatalf("应同步为 bound 并写入实际 UID,实际 %+v", saved)
}
if saved.MismatchAt != nil || saved.MismatchReason != "" {
t.Fatalf("同步为 bound 后应清空绑定错误标记,实际 %+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{}{
"binding": map[string]interface{}{
"bound": true,
"role": "痛写心扉",
"roleId": "3132710334",
"region": "安卓-微信",
},
})
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
token := model.RockToken{
UpstreamTokenID: "9003",
RedeemCode: "CODE-NOUID",
Color: model.RockColorBlack,
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.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)
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": "2026-10-29 12:00:00",
},
"deliveryList": []map[string]interface{}{
{"fulfillId": 4519960, "time": "2026-10-29T12:00:00.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",
}
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_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",
},
})
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_AbnormalKeepsManualRemark(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 || saved.Remark != "客服人工备注" {
t.Fatalf("应保留人工备注并标记 abnormal,实际 %+v", saved)
}
}