Files
hi-server/queue/logic/lottery/expireClaimLogic_test.go
T
shanshanzhong147 07409eb602 新功能(#4): 抽奖 Stage 2 人工奖领奖工单(crypto / physical / manual_other)
Closes HIF-4

Stage 2 交付:人工奖领奖工单完整闭环。crypto / physical / manual_other 三类奖品从抽中到 mark-paid 的全流程可用。

- 迁移 02159_lottery_claim:UNIQUE(draw_id) + 3 支持索引,状态机 pending_claim→reviewing→paying→paid,rejected 可复活,超时 expired
- 3 个 PrizeHandler:Dispatch→ErrDispatchNotSupported 兜底、ClaimSchema 各自形态、ValidateClaim 表驱动
- BuildCryptoClaimSchema:抽中时按奖品 config.networks 注入 enum,前端下拉直接可用
- Draw service dispatchOrEnqueueClaim:人工奖同 tx 插 pending_claim(回滚双清),nonce 重放回读 ExpiresAt + ClaimFormSchema
- POST /claim 实装:ownership 校验 → prize 类型校验 → handler.ValidateClaim → crypto network 白名单二次校验 → tx CAS status IN (pending_claim, rejected) AND expires_at > now
- Admin CRUD 5 接口:list(IN 批拉 snap + user,无 N+1)、summary(GROUP BY 一次拿计数 + overdue 单查)、approve/reject/mark-paid 全走 CAS + audit
- Scheduler @every 1h 扫过期,级联 lottery_draw.dispatch_state → expired
- 新增错误码 100005-100011(already_submitted / invalid_claim_data / draw_not_found / not_your_draw / claim_expired / claim_state_invalid)
- Rebase 后 Stage 2 测试主动 reuse PR E 的 unmetReasonsNotEmpty + evaluatedAtNotZero matcher,人工奖分支若绕过守卫会立即挂
- Stage 1 全部 4 处 guardrail 后端 rebase 时自检过:UnmetReasons、EvaluatedAt、GrantLedger.Payload、AdminMetaMiddleware 全保留

CI 全绿;28 files, +2442/-126;覆盖率 handler 78.9% / model.lottery 74.3% / draw 68.7% / queue/lottery 76.9%
2026-07-10 04:01:34 -07:00

70 lines
2.1 KiB
Go

// expireClaimLogic_test.go — 用 sqlmock 断言 SQL 契约(不涉及真实 DB)。
package lotteryLogic
import (
"context"
"errors"
"strings"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/hibiken/asynq"
"github.com/perfect-panel/server/internal/svc"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func newTestDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, func()) {
t.Helper()
sqlDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherFunc(func(expected, actual string) error {
if strings.Contains(actual, expected) {
return nil
}
return errors.New("actual sql does not contain expected: " + expected)
})))
if err != nil {
t.Fatalf("sqlmock: %v", err)
}
db, err := gorm.Open(mysql.New(mysql.Config{Conn: sqlDB, SkipInitializeWithVersion: true}), &gorm.Config{SkipDefaultTransaction: true})
if err != nil {
_ = sqlDB.Close()
t.Fatalf("gorm: %v", err)
}
return db, mock, func() { _ = sqlDB.Close() }
}
func TestExpireClaimLogic_NoRowsAffectedSkipsSecondUpdate(t *testing.T) {
db, mock, cleanup := newTestDB(t)
defer cleanup()
// UPDATE lottery_claim 但 RowsAffected == 0 → 应直接返回,不再打 UPDATE lottery_draw
mock.ExpectExec("UPDATE `lottery_claim`").
WillReturnResult(sqlmock.NewResult(0, 0))
logic := NewExpireClaimLogic(&svc.ServiceContext{DB: db})
if err := logic.ProcessTask(context.Background(), asynq.NewTask("", nil)); err != nil {
t.Fatalf("ProcessTask: %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet expectations: %v", err)
}
}
func TestExpireClaimLogic_ExpiresAndCascadesDraw(t *testing.T) {
db, mock, cleanup := newTestDB(t)
defer cleanup()
mock.ExpectExec("UPDATE `lottery_claim`").
WillReturnResult(sqlmock.NewResult(0, 3))
mock.ExpectExec("UPDATE `lottery_draw`").
WillReturnResult(sqlmock.NewResult(0, 3))
logic := NewExpireClaimLogic(&svc.ServiceContext{DB: db})
if err := logic.ProcessTask(context.Background(), asynq.NewTask("", nil)); err != nil {
t.Fatalf("ProcessTask: %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet expectations: %v", err)
}
}