Files

244 lines
7.8 KiB
Go

package service
import (
"context"
"encoding/json"
"errors"
"testing"
"affiliate_dash/internal/model"
)
func createDeleteTestToken(t *testing.T, svc *RockService, upstreamID, status string) model.RockToken {
t.Helper()
token := model.RockToken{
UpstreamTokenID: upstreamID,
Token: "tk-" + upstreamID,
RedeemCode: "rc-" + upstreamID,
Color: model.RockColorBlack,
Status: status,
}
if status == model.RockStatusUsing || status == model.RockStatusBound || status == model.RockStatusMismatch {
token.AssignedOrderNo = "ORDER-" + upstreamID
}
if err := svc.db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
return token
}
func TestDeleteRockTokens_SoftDeletesAndHidesFromList(t *testing.T) {
db := newRockTestDB(t)
svc := NewRockService(db, NewRockClient("https://example.com"))
ctx := context.Background()
available := createDeleteTestToken(t, svc, "90001", model.RockStatusAvailable)
redeemed := createDeleteTestToken(t, svc, "90002", model.RockStatusRedeemed)
result, err := svc.DeleteRockTokens(ctx, []uint{available.ID}, false, 7)
if err != nil {
t.Fatalf("删除 Token 失败: %v", err)
}
if result.Deleted != 1 || result.Requested != 1 {
t.Fatalf("删除数量不符: %+v", result)
}
_, total, err := svc.ListTokens(ctx, 1, 20, "", "")
if err != nil {
t.Fatalf("查询列表失败: %v", err)
}
if total != 1 {
t.Fatalf("软删除后列表应只剩 1 条,实际 %d", total)
}
stats, err := svc.GetStats(ctx)
if err != nil {
t.Fatalf("查询统计失败: %v", err)
}
if stats.AvailableStock != 0 || stats.RedeemedStock != 1 {
t.Fatalf("统计未排除软删除: available=%d redeemed=%d", stats.AvailableStock, stats.RedeemedStock)
}
var all []model.RockToken
if err := db.Unscoped().Order("id ASC").Find(&all).Error; err != nil {
t.Fatalf("Unscoped 查询失败: %v", err)
}
if len(all) != 2 {
t.Fatalf("软删除不应物理删除,实际记录数 %d", len(all))
}
var deleted model.RockToken
if err := db.Unscoped().First(&deleted, available.ID).Error; err != nil {
t.Fatalf("读取已删除记录失败: %v", err)
}
if !deleted.DeletedAt.Valid {
t.Fatal("available Token 未写入 deleted_at")
}
var active model.RockToken
if err := db.First(&active, available.ID).Error; err == nil {
t.Fatal("普通查询不应返回已软删除 Token")
}
_ = redeemed
}
func TestDeleteRockTokens_RequiresForceForDangerousStatuses(t *testing.T) {
db := newRockTestDB(t)
svc := NewRockService(db, NewRockClient("https://example.com"))
ctx := context.Background()
using := createDeleteTestToken(t, svc, "91001", model.RockStatusUsing)
bound := createDeleteTestToken(t, svc, "91002", model.RockStatusBound)
mismatch := createDeleteTestToken(t, svc, "91003", model.RockStatusMismatch)
ids := []uint{using.ID, bound.ID, mismatch.ID}
if _, err := svc.DeleteRockTokens(ctx, ids, false, 7); err == nil {
t.Fatal("包含危险状态时未强制确认应失败")
} else {
var forceErr *RockTokenForceRequiredError
if !errors.As(err, &forceErr) {
t.Fatalf("错误类型不是 ForceRequired: %T %v", err, err)
}
if forceErr.Dangerous != 3 {
t.Fatalf("危险数量不符: %d", forceErr.Dangerous)
}
}
var activeCount int64
if err := db.Model(&model.RockToken{}).Count(&activeCount).Error; err != nil {
t.Fatalf("统计失败: %v", err)
}
if activeCount != 3 {
t.Fatalf("强制确认失败时不应删除,实际剩余 %d", activeCount)
}
result, err := svc.DeleteRockTokens(ctx, ids, true, 7)
if err != nil {
t.Fatalf("强制删除失败: %v", err)
}
if result.Deleted != 3 {
t.Fatalf("强制删除数量不符: %+v", result)
}
}
func TestPreviewAndDeleteAllRockTokens_Filtered(t *testing.T) {
db := newRockTestDB(t)
svc := NewRockService(db, NewRockClient("https://example.com"))
ctx := context.Background()
createDeleteTestToken(t, svc, "92001", model.RockStatusRedeemed)
createDeleteTestToken(t, svc, "92002", model.RockStatusRedeemed)
createDeleteTestToken(t, svc, "92003", model.RockStatusAvailable)
preview, err := svc.PreviewRockTokenDelete(ctx, model.RockStatusRedeemed, "")
if err != nil {
t.Fatalf("预览失败: %v", err)
}
if preview.Total != 2 || preview.Dangerous != 0 || preview.Available != 0 {
t.Fatalf("预览数量不符: %+v", preview)
}
// 预览数量与当前数据不一致时必须拒绝,避免误删。
if _, err := svc.DeleteAllRockTokens(ctx, model.RockStatusRedeemed, "", 1, false, 7); err == nil {
t.Fatal("expected_count 不一致时应拒绝")
} else {
var conflict *RockTokenDeleteConflictError
if !errors.As(err, &conflict) {
t.Fatalf("错误类型不是 Conflict: %T %v", err, err)
}
}
result, err := svc.DeleteAllRockTokens(ctx, model.RockStatusRedeemed, "", 2, false, 7)
if err != nil {
t.Fatalf("删除全部失败: %v", err)
}
if result.Deleted != 2 {
t.Fatalf("删除数量不符: %+v", result)
}
_, total, err := svc.ListTokens(ctx, 1, 20, "", "")
if err != nil {
t.Fatalf("查询列表失败: %v", err)
}
if total != 1 {
t.Fatalf("按状态删除后应剩 1 条 available,实际 %d", total)
}
}
func TestPreviewDeleteAll_ReportsDangerousAndAvailable(t *testing.T) {
db := newRockTestDB(t)
svc := NewRockService(db, NewRockClient("https://example.com"))
ctx := context.Background()
createDeleteTestToken(t, svc, "93001", model.RockStatusAvailable)
createDeleteTestToken(t, svc, "93002", model.RockStatusBound)
createDeleteTestToken(t, svc, "93003", model.RockStatusRedeemed)
preview, err := svc.PreviewRockTokenDelete(ctx, "", "")
if err != nil {
t.Fatalf("预览失败: %v", err)
}
if preview.Total != 3 || preview.Dangerous != 1 || preview.Available != 1 || !preview.RequiresForce {
t.Fatalf("预览结果不符: %+v", preview)
}
if len(preview.DangerousItems) != 1 || preview.DangerousItems[0].Status != model.RockStatusBound {
t.Fatalf("危险明细不符: %+v", preview.DangerousItems)
}
}
func TestImportPackageDetail_DoesNotResurrectDeletedToken(t *testing.T) {
db := newRockTestDB(t)
svc := NewRockService(db, NewRockClient("https://example.com"))
ctx := context.Background()
packageID := int64(12345)
token := model.RockToken{
UpstreamTokenID: "94001",
Token: "tk-94001",
RedeemCode: "rc-94001",
Color: model.RockColorBlack,
Status: model.RockStatusAvailable,
PackageID: &packageID,
Source: "package",
}
if err := db.Create(&token).Error; err != nil {
t.Fatalf("创建 Token 失败: %v", err)
}
if _, err := svc.DeleteRockTokens(ctx, []uint{token.ID}, false, 7); err != nil {
t.Fatalf("软删除失败: %v", err)
}
var detail RockPackageDetailResponse
raw := `{
"package": {"id": 12345, "share_code": "share-test"},
"tokens": [
{"id": 94001, "token": "tk-94001", "redeem_code": "rc-94001", "color": "black", "status": "available", "created_at": "2026-10-31 10:00:00"}
]
}`
if err := json.Unmarshal([]byte(raw), &detail); err != nil {
t.Fatalf("解析包详情失败: %v", err)
}
inserted, updated, skipped, err := svc.importPackageDetail(ctx, &detail, nil)
if err != nil {
t.Fatalf("导入包详情失败: %v", err)
}
if inserted != 0 || updated != 0 || skipped != 1 {
t.Fatalf("已删除 Token 不应被导入: inserted=%d updated=%d skipped=%d", inserted, updated, skipped)
}
var activeCount int64
if err := db.Model(&model.RockToken{}).Where("upstream_token_id = ?", "94001").Count(&activeCount).Error; err != nil {
t.Fatalf("统计失败: %v", err)
}
if activeCount != 0 {
t.Fatalf("已删除 Token 被重新创建,active=%d", activeCount)
}
var total int64
if err := db.Unscoped().Model(&model.RockToken{}).Where("upstream_token_id = ?", "94001").Count(&total).Error; err != nil {
t.Fatalf("Unscoped 统计失败: %v", err)
}
if total != 1 {
t.Fatalf("软删除记录应保留一条,实际 %d", total)
}
}