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

378 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 未写入")
}
}
func TestRefreshTokenBinding_MarksMismatch(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.RockStatusMismatch || !strings.Contains(res.Message, "不一致") {
t.Fatalf("期望 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.MismatchAt == nil || !strings.Contains(saved.MismatchReason, "3132710334") {
t.Fatalf("数据库 mismatch 字段不符: %+v", saved)
}
}
func TestRefreshTokenBinding_EmptyExpectedUIDKeepsStatus(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.RockStatusUsing {
t.Fatalf("缺少预期 UID 时状态应保持 using,实际 %s", res.Status)
}
if !strings.Contains(res.Message, "人工核对") {
t.Fatalf("应提示人工核对,实际 %q", res.Message)
}
if !res.Warning {
t.Fatalf("缺少预期 UID 时应返回 warning=true")
}
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 != "3132710334" {
t.Fatalf("只应同步角色信息,实际 %+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)
}
}