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

313 lines
10 KiB
Go

package service
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"affiliate_dash/internal/model"
)
func TestUpdateConfig_BoundWarnDaysAndAutoRefresh(t *testing.T) {
db := newRockTestDB(t)
svc := NewRockService(db, NewRockClient("https://example.com"))
ctx := context.Background()
cfg, err := svc.GetConfig(ctx)
if err != nil {
t.Fatalf("获取配置失败: %v", err)
}
if cfg.BoundWarnDays != 2 {
t.Fatalf("绑定过期预警默认值应为 2,实际 %d", cfg.BoundWarnDays)
}
if cfg.AutoRefreshMismatchEnabled {
t.Fatalf("自动刷新开关默认必须关闭")
}
zero := 0
if _, err := svc.UpdateConfig(ctx, UpdateRockConfigReq{BoundWarnDays: &zero}); err == nil {
t.Fatalf("绑定过期预警天数小于 1 时必须报错")
}
tooMany := 366
if _, err := svc.UpdateConfig(ctx, UpdateRockConfigReq{BoundWarnDays: &tooMany}); err == nil {
t.Fatalf("绑定过期预警天数大于 365 时必须报错")
}
seven := 7
enabled := true
updated, err := svc.UpdateConfig(ctx, UpdateRockConfigReq{
BoundWarnDays: &seven,
AutoRefreshMismatchEnabled: &enabled,
})
if err != nil {
t.Fatalf("保存绑定过期预警失败: %v", err)
}
if updated.BoundWarnDays != 7 || !updated.AutoRefreshMismatchEnabled {
t.Fatalf("绑定过期预警/自动刷新未保存: %+v", updated)
}
}
func TestListTokens_OverdueFilters(t *testing.T) {
db := newRockTestDB(t)
svc := NewRockService(db, NewRockClient("https://example.com"))
ctx := context.Background()
now := time.Now()
old := now.AddDate(0, 0, -10)
fresh := now.Add(-time.Hour)
usingOld := model.RockToken{UpstreamTokenID: "overdue-using", RedeemCode: "C1", Status: model.RockStatusUsing, UsingAt: &old}
usingFresh := model.RockToken{UpstreamTokenID: "fresh-using", RedeemCode: "C2", Status: model.RockStatusUsing, UsingAt: &fresh}
boundOld := model.RockToken{UpstreamTokenID: "overdue-bound", RedeemCode: "C3", Status: model.RockStatusBound, BoundAt: &old}
boundFresh := model.RockToken{UpstreamTokenID: "fresh-bound", RedeemCode: "C4", Status: model.RockStatusBound, BoundAt: &fresh}
redeemedOld := model.RockToken{UpstreamTokenID: "old-redeemed", RedeemCode: "C5", Status: model.RockStatusRedeemed, BoundAt: &old}
for _, token := range []*model.RockToken{&usingOld, &usingFresh, &boundOld, &boundFresh, &redeemedOld} {
if err := db.Create(token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
}
cases := []struct {
status string
wantID uint
}{
{rockTokenStatusUsingOverdue, usingOld.ID},
{rockTokenStatusBoundOverdue, boundOld.ID},
}
for _, tc := range cases {
list, total, err := svc.ListTokens(ctx, 1, 20, tc.status, "")
if err != nil {
t.Fatalf("查询 %s 失败: %v", tc.status, err)
}
if total != 1 || len(list) != 1 || list[0].ID != tc.wantID {
t.Fatalf("%s 应只返回 ID=%d,实际 total=%d list=%+v", tc.status, tc.wantID, total, list)
}
}
stats, err := svc.GetStats(ctx)
if err != nil {
t.Fatalf("获取统计失败: %v", err)
}
if stats.UsingOverdueStock != 1 || stats.UsingDangerStock != 1 || stats.BoundOverdueStock != 1 {
t.Fatalf("过期预警统计不符: using_overdue=%d using_danger=%d bound_overdue=%d",
stats.UsingOverdueStock, stats.UsingDangerStock, stats.BoundOverdueStock)
}
}
func TestRefreshStaleTokens_SkipsWhenAutoRefreshDisabled(t *testing.T) {
db := newRockTestDB(t)
var calls int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&calls, 1)
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
token := model.RockToken{
UpstreamTokenID: "scan-off",
RedeemCode: "CODE-SCAN-OFF",
Status: model.RockStatusMismatch,
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
if err := db.Model(&model.RockToken{}).Where("id = ?", token.ID).
UpdateColumn("updated_at", time.Now().Add(-time.Hour)).Error; err != nil {
t.Fatalf("调整 updated_at 失败: %v", err)
}
svc.RefreshStaleTokens(context.Background())
if atomic.LoadInt32(&calls) != 0 {
t.Fatalf("自动刷新关闭时不应调用上游")
}
var saved model.RockToken
if err := db.First(&saved, token.ID).Error; err != nil {
t.Fatalf("读取 Token 失败: %v", err)
}
if saved.Status != model.RockStatusMismatch {
t.Fatalf("自动刷新关闭时应保持 mismatch,实际 %s", saved.Status)
}
}
func TestRefreshAllStaleTokens_FixesMismatch(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",
},
})
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
token := model.RockToken{
UpstreamTokenID: "scan-on",
RedeemCode: "CODE-SCAN-ON",
Status: model.RockStatusMismatch,
ExpectedUID: "1111111111",
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
if err := db.Model(&model.RockToken{}).Where("id = ?", token.ID).
UpdateColumn("updated_at", time.Now().Add(-time.Hour)).Error; err != nil {
t.Fatalf("调整 updated_at 失败: %v", err)
}
result, err := svc.RefreshAllStaleTokens(context.Background())
if err != nil {
t.Fatalf("一键刷新失败: %v", err)
}
if result.Scanned != 1 || result.Fixed != 1 || result.Remaining != 0 || result.Failed != 0 {
t.Fatalf("一键刷新结果不符: %+v", result)
}
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.MismatchAt != nil {
t.Fatalf("应修复为 bound 并清空 mismatch 标记,实际 %+v", saved)
}
}
// 手动一键刷新除了绑定错误,还要覆盖「使用超时」和「绑定超时」两类 Token。
func TestRefreshAllStaleTokens_IncludesOverdueTokens(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-USING-OLD/refresh":
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"token": map[string]interface{}{
"status": "bound",
"bound_account": "新角色",
"bound_account_id": "10001",
},
})
case "/api/redeem/CODE-BOUND-OLD/refresh":
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"token": map[string]interface{}{
"status": "redeemed",
"bound_account": "旧角色",
"bound_account_id": "10002",
"redeemed_at": "2026-11-07 12:00:00",
},
})
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
old := time.Now().AddDate(0, 0, -10)
usingToken := model.RockToken{
UpstreamTokenID: "stale-using",
RedeemCode: "CODE-USING-OLD",
Status: model.RockStatusUsing,
UsingAt: &old,
}
boundToken := model.RockToken{
UpstreamTokenID: "stale-bound",
RedeemCode: "CODE-BOUND-OLD",
Status: model.RockStatusBound,
BoundAt: &old,
}
for _, token := range []*model.RockToken{&usingToken, &boundToken} {
if err := db.Create(token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
if err := db.Model(&model.RockToken{}).Where("id = ?", token.ID).
UpdateColumn("updated_at", time.Now().Add(-time.Hour)).Error; err != nil {
t.Fatalf("调整 updated_at 失败: %v", err)
}
}
result, err := svc.RefreshAllStaleTokens(context.Background())
if err != nil {
t.Fatalf("一键刷新失败: %v", err)
}
if result.Scanned != 2 || result.Fixed != 2 || result.Remaining != 0 || result.Failed != 0 {
t.Fatalf("一键刷新结果不符: %+v", result)
}
var savedUsing, savedBound model.RockToken
if err := db.First(&savedUsing, usingToken.ID).Error; err != nil {
t.Fatalf("读取使用超时 Token 失败: %v", err)
}
if savedUsing.Status != model.RockStatusBound {
t.Fatalf("使用超时 Token 应同步为 bound,实际 %s", savedUsing.Status)
}
if err := db.First(&savedBound, boundToken.ID).Error; err != nil {
t.Fatalf("读取绑定超时 Token 失败: %v", err)
}
if savedBound.Status != model.RockStatusRedeemed || savedBound.RedeemedAt == nil {
t.Fatalf("绑定超时 Token 应同步为 redeemed,实际 %+v", savedBound)
}
}
// 自动刷新与一键刷新同规则:开关开启后也要覆盖使用超时 Token。
func TestRefreshStaleTokens_IncludesOverdueWhenEnabled(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": "20001",
},
})
}))
defer server.Close()
svc := NewRockService(db, NewRockClient(server.URL))
setRockTestBearer(t, svc, "bearer-test")
enabled := true
if _, err := svc.UpdateConfig(context.Background(), UpdateRockConfigReq{AutoRefreshMismatchEnabled: &enabled}); err != nil {
t.Fatalf("开启自动刷新失败: %v", err)
}
old := time.Now().AddDate(0, 0, -10)
token := model.RockToken{
UpstreamTokenID: "auto-stale-using",
RedeemCode: "CODE-AUTO-OLD",
Status: model.RockStatusUsing,
UsingAt: &old,
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
if err := db.Model(&model.RockToken{}).Where("id = ?", token.ID).
UpdateColumn("updated_at", time.Now().Add(-time.Hour)).Error; err != nil {
t.Fatalf("调整 updated_at 失败: %v", err)
}
svc.RefreshStaleTokens(context.Background())
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)
}
}