313 lines
10 KiB
Go
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)
|
|
}
|
|
}
|