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

383 lines
12 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)
}
}