289 lines
10 KiB
Go
289 lines
10 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"affiliate_dash/internal/model"
|
|
)
|
|
|
|
func setRockTestBearer(t *testing.T, svc *RockService, token string) {
|
|
t.Helper()
|
|
bearer := token
|
|
if _, err := svc.UpdateConfig(context.Background(), UpdateRockConfigReq{BearerToken: &bearer}); err != nil {
|
|
t.Fatalf("设置 Bearer Token 失败: %v", err)
|
|
}
|
|
// 测试默认给足积分,避免降级逻辑把采购量算成 0;积分限制由专门用例覆盖。
|
|
if err := svc.db.Model(&model.RockConfig{}).Where("id = 1").Update("points", 100000).Error; err != nil {
|
|
t.Fatalf("设置测试积分失败: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPurchaseTokens_CreatesPackageAndImportsAvailable(t *testing.T) {
|
|
db := newRockTestDB(t)
|
|
|
|
var gotClaimCount string
|
|
var gotClaimColor string
|
|
var gotIDs []string
|
|
var gotNote string
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch {
|
|
case r.URL.Path == "/api/user/dashboard":
|
|
writeRockDashboardJSON(w, 999, "20.0")
|
|
case r.URL.Path == "/api/user/tokens/claim":
|
|
var body struct {
|
|
Color string `json:"color"`
|
|
Count string `json:"count"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
gotClaimColor = body.Color
|
|
gotClaimCount = body.Count
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"tokens": []map[string]interface{}{
|
|
{"id": 30001, "token": "token-1", "color": "black", "region": "Q/V可用", "status": "using", "redeem_code": "code-1"},
|
|
{"id": 30002, "token": "token-2", "color": "black", "region": "Q/V可用", "status": "using", "redeem_code": "code-2"},
|
|
},
|
|
})
|
|
case r.URL.Path == "/api/user/packages" && r.Method == http.MethodPost:
|
|
var body struct {
|
|
IDs []string `json:"ids"`
|
|
Note string `json:"note"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
gotIDs = body.IDs
|
|
gotNote = body.Note
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"package": map[string]interface{}{
|
|
"id": 9001, "token_count": 2, "share_code": "share123", "status": "active", "note": body.Note,
|
|
},
|
|
})
|
|
case r.URL.Path == "/api/package/share123":
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"package": map[string]interface{}{"id": 9001, "token_count": 2, "share_code": "share123", "status": "active"},
|
|
"tokens": []map[string]interface{}{
|
|
{"id": 30001, "token": "token-1", "color": "black", "region": "Q/V可用", "status": "using", "redeem_code": "code-1", "created_at": "2026-10-29 13:45:27"},
|
|
{"id": 30002, "token": "token-2", "color": "black", "region": "Q/V可用", "status": "using", "redeem_code": "code-2", "created_at": "2026-10-29 13:45:27"},
|
|
},
|
|
})
|
|
case r.URL.Path == "/api/me":
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"user": map[string]interface{}{"username": "tester", "points": "880.0"},
|
|
})
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
svc := NewRockService(db, NewRockClient(server.URL))
|
|
setRockTestBearer(t, svc, "bearer-test")
|
|
|
|
tokens, err := svc.PurchaseTokens(context.Background(), 2, true)
|
|
if err != nil {
|
|
t.Fatalf("采购失败: %v", err)
|
|
}
|
|
if len(tokens) != 2 {
|
|
t.Fatalf("期望返回 2 个 Token,实际 %d", len(tokens))
|
|
}
|
|
if gotClaimColor != model.RockColorBlack || gotClaimCount != "2" {
|
|
t.Fatalf("claim 参数不符: color=%s count=%s", gotClaimColor, gotClaimCount)
|
|
}
|
|
if len(gotIDs) != 2 || gotIDs[0] != "30001" || gotIDs[1] != "30002" {
|
|
t.Fatalf("打包 ids 不符: %v", gotIDs)
|
|
}
|
|
if !strings.HasPrefix(gotNote, "补货-") {
|
|
t.Fatalf("自动补货 note 前缀不符: %s", gotNote)
|
|
}
|
|
|
|
var rows []model.RockToken
|
|
if err := db.Order("id ASC").Find(&rows).Error; err != nil {
|
|
t.Fatalf("查询 Token 失败: %v", err)
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Fatalf("期望本地 2 条 Token,实际 %d", len(rows))
|
|
}
|
|
for _, row := range rows {
|
|
if row.Status != model.RockStatusAvailable {
|
|
t.Fatalf("新 Token 状态应为 available,实际 %s", row.Status)
|
|
}
|
|
if row.Source != "package" {
|
|
t.Fatalf("新 Token 来源应为 package,实际 %s", row.Source)
|
|
}
|
|
if row.PackageID == nil || *row.PackageID != 9001 {
|
|
t.Fatalf("package_id 未写入: %+v", row.PackageID)
|
|
}
|
|
if row.PackageShareCode != "share123" {
|
|
t.Fatalf("package_share_code 未写入,实际 %q", row.PackageShareCode)
|
|
}
|
|
if row.Token == "" || row.RedeemCode == "" {
|
|
t.Fatalf("提取码/兑换码未写入: %+v", row)
|
|
}
|
|
}
|
|
|
|
cfg, _ := svc.GetConfig(context.Background())
|
|
if cfg.LastError != "" {
|
|
t.Fatalf("成功后应清空 last_error,实际 %q", cfg.LastError)
|
|
}
|
|
if cfg.LastPurchasedAt == nil || cfg.LastAttemptAt == nil {
|
|
t.Fatalf("last_purchased_at/last_attempt_at 未记录")
|
|
}
|
|
}
|
|
|
|
func TestPurchaseTokens_PackageFailureFallsBackToPurchase(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/user/dashboard":
|
|
writeRockDashboardJSON(w, 999, "20.0")
|
|
case "/api/user/tokens/claim":
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"tokens": []map[string]interface{}{
|
|
{"id": 31001, "token": "token-1", "color": "black", "region": "Q/V可用", "status": "using", "redeem_code": "code-1"},
|
|
},
|
|
})
|
|
case "/api/user/packages":
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte(`{"error":"package down"}`))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
svc := NewRockService(db, NewRockClient(server.URL))
|
|
setRockTestBearer(t, svc, "bearer-test")
|
|
|
|
tokens, err := svc.PurchaseTokens(context.Background(), 1, false)
|
|
if err != nil {
|
|
t.Fatalf("打包失败时应兜底成功,实际报错: %v", err)
|
|
}
|
|
if len(tokens) != 1 {
|
|
t.Fatalf("期望兜底入库 1 个,实际 %d", len(tokens))
|
|
}
|
|
|
|
var row model.RockToken
|
|
if err := db.First(&row).Error; err != nil {
|
|
t.Fatalf("查询 Token 失败: %v", err)
|
|
}
|
|
if row.Status != model.RockStatusAvailable || row.Source != "purchase" || row.PackageID != nil {
|
|
t.Fatalf("兜底入库字段不符: status=%s source=%s package_id=%v", row.Status, row.Source, row.PackageID)
|
|
}
|
|
if row.PackageShareCode != "" {
|
|
t.Fatalf("打包创建失败时不应有分享码,实际 %q", row.PackageShareCode)
|
|
}
|
|
|
|
cfg, _ := svc.GetConfig(context.Background())
|
|
if !strings.Contains(cfg.LastError, "创建上游打包记录失败") {
|
|
t.Fatalf("last_error 应记录打包失败,实际 %q", cfg.LastError)
|
|
}
|
|
}
|
|
|
|
func TestPurchaseTokens_PackageDetailFailureFallsBackWithPackageID(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/user/dashboard":
|
|
writeRockDashboardJSON(w, 999, "20.0")
|
|
case "/api/user/tokens/claim":
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"tokens": []map[string]interface{}{
|
|
{"id": 32001, "token": "token-1", "color": "black", "region": "Q/V可用", "status": "using", "redeem_code": "code-1"},
|
|
},
|
|
})
|
|
case "/api/user/packages":
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"package": map[string]interface{}{"id": 9002, "token_count": 1, "share_code": "detailfail", "status": "active"},
|
|
})
|
|
case "/api/package/detailfail":
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte(`{"error":"detail down"}`))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
svc := NewRockService(db, NewRockClient(server.URL))
|
|
setRockTestBearer(t, svc, "bearer-test")
|
|
|
|
tokens, err := svc.PurchaseTokens(context.Background(), 1, false)
|
|
if err != nil {
|
|
t.Fatalf("详情失败时应兜底成功,实际报错: %v", err)
|
|
}
|
|
if len(tokens) != 1 {
|
|
t.Fatalf("期望兜底入库 1 个,实际 %d", len(tokens))
|
|
}
|
|
|
|
var row model.RockToken
|
|
if err := db.First(&row).Error; err != nil {
|
|
t.Fatalf("查询 Token 失败: %v", err)
|
|
}
|
|
if row.Status != model.RockStatusAvailable || row.Source != "package" {
|
|
t.Fatalf("兜底入库字段不符: status=%s source=%s", row.Status, row.Source)
|
|
}
|
|
if row.PackageID == nil || *row.PackageID != 9002 {
|
|
t.Fatalf("兜底入库应保留 package_id=9002,实际 %v", row.PackageID)
|
|
}
|
|
if row.PackageShareCode != "detailfail" {
|
|
t.Fatalf("兜底入库应保留 package_share_code=detailfail,实际 %q", row.PackageShareCode)
|
|
}
|
|
|
|
cfg, _ := svc.GetConfig(context.Background())
|
|
if !strings.Contains(cfg.LastError, "详情拉取失败") {
|
|
t.Fatalf("last_error 应记录详情失败,实际 %q", cfg.LastError)
|
|
}
|
|
}
|
|
|
|
func TestAutoPurchase_FailureCooldown(t *testing.T) {
|
|
db := newRockTestDB(t)
|
|
var claimCalls int32
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if r.URL.Path == "/api/user/dashboard" {
|
|
writeRockDashboardJSON(w, 999, "20.0")
|
|
return
|
|
}
|
|
if r.URL.Path == "/api/user/tokens/claim" {
|
|
atomic.AddInt32(&claimCalls, 1)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte(`{"error":"insufficient points"}`))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer server.Close()
|
|
|
|
svc := NewRockService(db, NewRockClient(server.URL))
|
|
setRockTestBearer(t, svc, "bearer-test")
|
|
|
|
if _, err := svc.PurchaseTokens(context.Background(), 50, true); err == nil {
|
|
t.Fatalf("第一次补货应因上游 500 失败")
|
|
}
|
|
if _, err := svc.PurchaseTokens(context.Background(), 50, true); err == nil || !strings.Contains(err.Error(), "冷却中") {
|
|
t.Fatalf("失败后第二次应立即命中冷却,实际 err=%v", err)
|
|
}
|
|
if calls := atomic.LoadInt32(&claimCalls); calls != 1 {
|
|
t.Fatalf("冷却期内不应再次请求上游 claim,实际调用 %d 次", calls)
|
|
}
|
|
|
|
cfg, _ := svc.GetConfig(context.Background())
|
|
if cfg.LastAttemptAt == nil {
|
|
t.Fatalf("last_attempt_at 未记录")
|
|
}
|
|
if cfg.LastAutoAttemptAt == nil {
|
|
t.Fatalf("last_auto_attempt_at 未记录")
|
|
}
|
|
if !strings.Contains(cfg.LastError, "调用上游采购接口失败") {
|
|
t.Fatalf("last_error 未记录采购失败: %q", cfg.LastError)
|
|
}
|
|
}
|