// 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) } }