222 lines
7.3 KiB
Go
222 lines
7.3 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"affiliate_dash/internal/model"
|
|
)
|
|
|
|
func restoreSkipReasons(items []RockTokenRestoreSkipItem) map[uint]string {
|
|
reasons := make(map[uint]string, len(items))
|
|
for _, item := range items {
|
|
reasons[item.ID] = item.Reason
|
|
}
|
|
return reasons
|
|
}
|
|
|
|
func TestRestoreRockTokens_UnhidesDeletedToken(t *testing.T) {
|
|
db := newRockTestDB(t)
|
|
svc := NewRockService(db, NewRockClient("https://example.com"))
|
|
ctx := context.Background()
|
|
|
|
available := createDeleteTestToken(t, svc, "95001", model.RockStatusAvailable)
|
|
redeemed := createDeleteTestToken(t, svc, "95002", model.RockStatusRedeemed)
|
|
if _, err := svc.DeleteRockTokens(ctx, []uint{available.ID, redeemed.ID}, false, 7); err != nil {
|
|
t.Fatalf("软删除失败: %v", err)
|
|
}
|
|
|
|
deleted, total, err := svc.ListDeletedRockTokens(ctx, 1, 20, "", "")
|
|
if err != nil {
|
|
t.Fatalf("查询回收站失败: %v", err)
|
|
}
|
|
if total != 2 || len(deleted) != 2 {
|
|
t.Fatalf("回收站应有 2 条,实际 total=%d len=%d", total, len(deleted))
|
|
}
|
|
var deletedAtSet bool
|
|
for _, row := range deleted {
|
|
if row.DeletedAt.Valid && row.DeletedAtText != "" {
|
|
deletedAtSet = true
|
|
}
|
|
}
|
|
if !deletedAtSet {
|
|
t.Fatal("回收站列表应带出 deleted_at 字符串")
|
|
}
|
|
|
|
stats, err := svc.GetStats(ctx)
|
|
if err != nil {
|
|
t.Fatalf("查询统计失败: %v", err)
|
|
}
|
|
if stats.DeletedStock != 2 || stats.AvailableStock != 0 {
|
|
t.Fatalf("删除后统计不符: deleted=%d available=%d", stats.DeletedStock, stats.AvailableStock)
|
|
}
|
|
|
|
result, err := svc.RestoreRockTokens(ctx, []uint{available.ID}, 7)
|
|
if err != nil {
|
|
t.Fatalf("恢复失败: %v", err)
|
|
}
|
|
if result.Requested != 1 || result.Restored != 1 || result.Skipped != 0 {
|
|
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 != 1 || stats.DeletedStock != 1 {
|
|
t.Fatalf("恢复后统计不符: available=%d deleted=%d", stats.AvailableStock, stats.DeletedStock)
|
|
}
|
|
|
|
// 状态筛选同样作用于回收站。
|
|
deleted, total, err = svc.ListDeletedRockTokens(ctx, 1, 20, model.RockStatusRedeemed, "")
|
|
if err != nil {
|
|
t.Fatalf("按状态查询回收站失败: %v", err)
|
|
}
|
|
if total != 1 || len(deleted) != 1 || deleted[0].ID != redeemed.ID {
|
|
t.Fatalf("回收站状态筛选不符: total=%d %+v", total, deleted)
|
|
}
|
|
}
|
|
|
|
func TestRestoreRockTokens_SkipsActiveRecords(t *testing.T) {
|
|
db := newRockTestDB(t)
|
|
svc := NewRockService(db, NewRockClient("https://example.com"))
|
|
ctx := context.Background()
|
|
|
|
active := createDeleteTestToken(t, svc, "96001", model.RockStatusAvailable)
|
|
usingToken := createDeleteTestToken(t, svc, "96002", model.RockStatusUsing)
|
|
if _, err := svc.DeleteRockTokens(ctx, []uint{usingToken.ID}, true, 7); err != nil {
|
|
t.Fatalf("强制删除失败: %v", err)
|
|
}
|
|
|
|
result, err := svc.RestoreRockTokens(ctx, []uint{active.ID, usingToken.ID, 999999}, 7)
|
|
if err != nil {
|
|
t.Fatalf("恢复失败: %v", err)
|
|
}
|
|
if result.Requested != 3 || result.Restored != 1 || result.Skipped != 2 {
|
|
t.Fatalf("部分恢复结果不符: %+v", result)
|
|
}
|
|
if len(result.StatusCounts) != 1 || result.StatusCounts[0].Status != model.RockStatusUsing {
|
|
t.Fatalf("恢复状态分布不符: %+v", result.StatusCounts)
|
|
}
|
|
reasons := restoreSkipReasons(result.SkippedItems)
|
|
if reasons[active.ID] != RockRestoreSkipNotDeleted {
|
|
t.Fatalf("未删除记录应跳过 not_deleted,实际 %q", reasons[active.ID])
|
|
}
|
|
if reasons[999999] != RockRestoreSkipNotDeleted {
|
|
t.Fatalf("不存在的记录应跳过 not_deleted,实际 %q", reasons[999999])
|
|
}
|
|
|
|
var restored model.RockToken
|
|
if err := db.First(&restored, usingToken.ID).Error; err != nil {
|
|
t.Fatalf("恢复后的 Token 应可普通查询: %v", err)
|
|
}
|
|
if restored.Status != model.RockStatusUsing {
|
|
t.Fatalf("恢复后状态应保持 using,实际 %s", restored.Status)
|
|
}
|
|
|
|
deletedRows, total, err := svc.ListDeletedRockTokens(ctx, 1, 20, "", "")
|
|
if err != nil {
|
|
t.Fatalf("查询回收站失败: %v", err)
|
|
}
|
|
if total != 0 || len(deletedRows) != 0 {
|
|
t.Fatalf("回收站应已清空,实际 %d", total)
|
|
}
|
|
}
|
|
|
|
func TestRestoreRockTokens_SkipsOrderConflict(t *testing.T) {
|
|
db := newRockTestDB(t)
|
|
svc := NewRockService(db, NewRockClient("https://example.com"))
|
|
ctx := context.Background()
|
|
|
|
old := createDeleteTestToken(t, svc, "97001", model.RockStatusBound)
|
|
if _, err := svc.DeleteRockTokens(ctx, []uint{old.ID}, true, 7); err != nil {
|
|
t.Fatalf("强制删除失败: %v", err)
|
|
}
|
|
|
|
// 订单还没有被别的 Token 占用时,恢复应当成功。
|
|
result, err := svc.RestoreRockTokens(ctx, []uint{old.ID}, 7)
|
|
if err != nil {
|
|
t.Fatalf("无冲突恢复失败: %v", err)
|
|
}
|
|
if result.Restored != 1 || result.Skipped != 0 {
|
|
t.Fatalf("无冲突恢复结果不符: %+v", result)
|
|
}
|
|
|
|
// 删除后同一订单被新 Token 接管,再恢复旧 Token 必须跳过。
|
|
if _, err := svc.DeleteRockTokens(ctx, []uint{old.ID}, true, 7); err != nil {
|
|
t.Fatalf("再次删除失败: %v", err)
|
|
}
|
|
fresh := model.RockToken{
|
|
UpstreamTokenID: "97002",
|
|
Token: "tk-97002",
|
|
RedeemCode: "rc-97002",
|
|
Color: model.RockColorBlack,
|
|
Status: model.RockStatusBound,
|
|
AssignedOrderNo: old.AssignedOrderNo,
|
|
}
|
|
if err := db.Create(&fresh).Error; err != nil {
|
|
t.Fatalf("创建新 Token 失败: %v", err)
|
|
}
|
|
|
|
result, err = svc.RestoreRockTokens(ctx, []uint{old.ID}, 7)
|
|
if err != nil {
|
|
t.Fatalf("冲突恢复应返回部分成功而不是错误: %v", err)
|
|
}
|
|
if result.Restored != 0 || result.Skipped != 1 {
|
|
t.Fatalf("订单冲突应跳过: %+v", result)
|
|
}
|
|
if reasons := restoreSkipReasons(result.SkippedItems); reasons[old.ID] != RockRestoreSkipOrderConflict {
|
|
t.Fatalf("跳过原因应为 order_conflict,实际 %q", reasons[old.ID])
|
|
}
|
|
|
|
var stillDeleted model.RockToken
|
|
if err := db.Unscoped().First(&stillDeleted, old.ID).Error; err != nil {
|
|
t.Fatalf("读取旧记录失败: %v", err)
|
|
}
|
|
if !stillDeleted.DeletedAt.Valid {
|
|
t.Fatal("订单冲突时不应恢复旧 Token")
|
|
}
|
|
}
|
|
|
|
func TestRestoreRockTokens_SkipsDuplicateUpstream(t *testing.T) {
|
|
db := newRockTestDB(t)
|
|
svc := NewRockService(db, NewRockClient("https://example.com"))
|
|
ctx := context.Background()
|
|
|
|
old := createDeleteTestToken(t, svc, "98001", model.RockStatusAvailable)
|
|
if _, err := svc.DeleteRockTokens(ctx, []uint{old.ID}, false, 7); err != nil {
|
|
t.Fatalf("软删除失败: %v", err)
|
|
}
|
|
|
|
// 部分唯一索引允许已删除记录与新记录共用同一个 upstream_token_id。
|
|
fresh := model.RockToken{
|
|
UpstreamTokenID: "98001",
|
|
Token: "tk-98001-b",
|
|
RedeemCode: "rc-98001-b",
|
|
Color: model.RockColorBlack,
|
|
Status: model.RockStatusAvailable,
|
|
}
|
|
if err := db.Create(&fresh).Error; err != nil {
|
|
t.Fatalf("创建同编号新 Token 失败: %v", err)
|
|
}
|
|
|
|
result, err := svc.RestoreRockTokens(ctx, []uint{old.ID}, 7)
|
|
if err != nil {
|
|
t.Fatalf("恢复失败: %v", err)
|
|
}
|
|
if result.Restored != 0 || result.Skipped != 1 {
|
|
t.Fatalf("上游编号冲突应跳过: %+v", result)
|
|
}
|
|
if reasons := restoreSkipReasons(result.SkippedItems); reasons[old.ID] != RockRestoreSkipDuplicateUpstream {
|
|
t.Fatalf("跳过原因应为 duplicate_upstream,实际 %q", reasons[old.ID])
|
|
}
|
|
}
|