topfans/backend/services/taskService/repository/like_bet_repo_test.go
zerosaturation cc7794e0f4 test(task): consumer + daily_task repo/service + like_bet ON CONFLICT 修订
Phase F.6 测试覆盖:

- mq/consumer_test.go: MQ consumer 单元测试,用 fakeDailyTaskService 模拟
  ProcessTaskEvent(newHandleTaskEvent 唯一调用点)
- repository/daily_task_repo_test.go: DailyTaskRepository CRUD 单测
  (复用 like_bet_repo_test.go 的 TestMain + dev PG 15432 sentinel 模式)
- service/daily_task_service_test.go: DailyTaskService.ProcessTaskEvent 核心
  完成判定逻辑单测
- repository/like_bet_repo_test.go: UniqueConstraint 测试语义修订 —
  BatchCreate 内部用 clause.OnConflict{DoNothing: true} 实现幂等,
  重复 (exhibition_id, like_id) 不再报错,DB 仍保留 1 条记录

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:46:29 +08:00

220 lines
7.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repository
import (
"os"
"testing"
"time"
"github.com/topfans/backend/pkg/database"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/services/taskService/model"
"gorm.io/gorm"
)
// TestMain 初始化 logger + DB避免 logger.Logger nil + 测试可跳过连不上 DB 的环境)
func TestMain(m *testing.M) {
_ = logger.Init(logger.Config{
ServiceName: "task-service-repo-test",
Environment: "test",
LogLevel: "error",
})
os.Exit(m.Run())
}
// setupTestDB 连接 dev 库(与后端 .env.dev 配置一致15432 / postgres / 123456
// 注意:不要连 5432 / haihuizhu那是另一个本机 PostgreSQL 实例(无 uk_like_bet_unique 约束)
func setupTestDB(t *testing.T) *gorm.DB {
config := database.Config{
Host: "localhost",
Port: 15432,
User: "postgres",
Password: "123456",
DBName: "top-fans",
SSLMode: "disable",
TimeZone: "Asia/Shanghai",
}
if err := database.Init(config); err != nil {
t.Skipf("Skipping test: failed to connect to test database: %v", err)
}
db := database.GetDB()
if err := db.AutoMigrate(&model.LikeBetRevenueRecord{}); err != nil {
t.Logf("Warning: AutoMigrate like_bet_revenue_records: %v", err)
}
return db
}
// cleanupTestRecords 删除本测试产生的记录(用 user_id < 0 的 sentinel 避免污染线上数据)
func cleanupTestRecords(t *testing.T, db *gorm.DB) {
if err := db.Where("user_id < ?", 0).Delete(&model.LikeBetRevenueRecord{}).Error; err != nil {
t.Logf("Warning: cleanup like_bet_revenue_records: %v", err)
}
}
func TestLikeBetRevenueRepository_BatchCreate(t *testing.T) {
db := setupTestDB(t)
defer cleanupTestRecords(t, db)
repo := NewLikeBetRevenueRepository(db)
now := time.Now().UnixMilli()
records := []*model.LikeBetRevenueRecord{
{UserID: -1, StarID: 87, ExhibitionID: 90001, AssetID: 100, LikeID: 8001, BetOrder: 1, TotalLikes: 3, CrystalAmount: 3, CycleStartTime: now, CycleEndTime: now, Status: "claimable", CreatedAt: now},
{UserID: -2, StarID: 87, ExhibitionID: 90001, AssetID: 100, LikeID: 8002, BetOrder: 2, TotalLikes: 3, CrystalAmount: 2, CycleStartTime: now, CycleEndTime: now, Status: "claimable", CreatedAt: now},
{UserID: -3, StarID: 87, ExhibitionID: 90001, AssetID: 100, LikeID: 8003, BetOrder: 3, TotalLikes: 3, CrystalAmount: 1, CycleStartTime: now, CycleEndTime: now, Status: "claimable", CreatedAt: now},
}
if err := repo.BatchCreate(records); err != nil {
t.Fatalf("BatchCreate failed: %v", err)
}
// 验证
var got int64
if err := db.Model(&model.LikeBetRevenueRecord{}).Where("user_id IN ? AND exhibition_id = ?", []int64{-1, -2, -3}, 90001).Count(&got).Error; err != nil {
t.Fatalf("Count failed: %v", err)
}
if got != 3 {
t.Errorf("Expected 3 records, got %d", got)
}
}
func TestLikeBetRevenueRepository_ListByUser(t *testing.T) {
db := setupTestDB(t)
defer cleanupTestRecords(t, db)
repo := NewLikeBetRevenueRepository(db)
now := time.Now().UnixMilli()
// 给 user_id=-10, star_id=87 写 3 条
records := []*model.LikeBetRevenueRecord{
{UserID: -10, StarID: 87, ExhibitionID: 90010, AssetID: 110, LikeID: 8101, BetOrder: 1, TotalLikes: 5, CrystalAmount: 5, CycleStartTime: now, CycleEndTime: now, Status: "claimable", CreatedAt: now},
{UserID: -10, StarID: 87, ExhibitionID: 90011, AssetID: 111, LikeID: 8102, BetOrder: 2, TotalLikes: 5, CrystalAmount: 4, CycleStartTime: now, CycleEndTime: now, Status: "claimed", CreatedAt: now},
{UserID: -10, StarID: 87, ExhibitionID: 90012, AssetID: 112, LikeID: 8103, BetOrder: 3, TotalLikes: 5, CrystalAmount: 3, CycleStartTime: now, CycleEndTime: now, Status: "claimable", CreatedAt: now},
}
if err := repo.BatchCreate(records); err != nil {
t.Fatalf("BatchCreate failed: %v", err)
}
// 查全部
all, total, err := repo.ListByUser(-10, 87, "", 1, 10)
if err != nil {
t.Fatalf("ListByUser (all) failed: %v", err)
}
if total != 3 {
t.Errorf("Expected total=3, got %d", total)
}
if len(all) != 3 {
t.Errorf("Expected 3 items, got %d", len(all))
}
// 查 claimable
claimable, totalClaimable, err := repo.ListByUser(-10, 87, "claimable", 1, 10)
if err != nil {
t.Fatalf("ListByUser (claimable) failed: %v", err)
}
if totalClaimable != 2 {
t.Errorf("Expected total=2 claimable, got %d", totalClaimable)
}
if len(claimable) != 2 {
t.Errorf("Expected 2 claimable items, got %d", len(claimable))
}
// 跨 star_id 隔离:换 star_id 查不到
other, _, err := repo.ListByUser(-10, 999, "", 1, 10)
if err != nil {
t.Fatalf("ListByUser (other star) failed: %v", err)
}
if len(other) != 0 {
t.Errorf("Expected 0 items for other star_id, got %d", len(other))
}
}
func TestLikeBetRevenueRepository_ClaimRecord_OptimisticLock(t *testing.T) {
db := setupTestDB(t)
defer cleanupTestRecords(t, db)
repo := NewLikeBetRevenueRepository(db)
now := time.Now().UnixMilli()
rec := &model.LikeBetRevenueRecord{
UserID: -20, StarID: 87, ExhibitionID: 90020, AssetID: 120,
LikeID: 8201, BetOrder: 1, TotalLikes: 3, CrystalAmount: 3,
CycleStartTime: now, CycleEndTime: now, Status: "claimable", CreatedAt: now,
}
if err := repo.BatchCreate([]*model.LikeBetRevenueRecord{rec}); err != nil {
t.Fatalf("BatchCreate failed: %v", err)
}
// 第一次领取:成功
ok, err := repo.ClaimRecord(rec.ID, -20)
if err != nil {
t.Fatalf("ClaimRecord first time failed: %v", err)
}
if !ok {
t.Fatal("First claim should succeed")
}
// 第二次领取乐观锁拦截RowsAffected=0
ok2, err2 := repo.ClaimRecord(rec.ID, -20)
if err2 != nil {
t.Fatalf("ClaimRecord second time returned error: %v", err2)
}
if ok2 {
t.Error("Second claim should be blocked by optimistic lock")
}
// 错误 user_id不应成功
ok3, err3 := repo.ClaimRecord(rec.ID, -999)
if err3 != nil {
t.Fatalf("ClaimRecord wrong user returned error: %v", err3)
}
if ok3 {
t.Error("Wrong user claim should be blocked")
}
// 验证最终状态
got, err := repo.GetRecord(rec.ID)
if err != nil {
t.Fatalf("GetRecord failed: %v", err)
}
if got.Status != "claimed" {
t.Errorf("Expected status=claimed, got %s", got.Status)
}
}
func TestLikeBetRevenueRepository_UniqueConstraint(t *testing.T) {
// 验证 (exhibition_id, like_id) 唯一约束:
// - 第一次 BatchCreate 成功
// - 第二次同样 (exhibition_id, like_id) 的 BatchCreate 应**不报错**silent skip
// 由 BatchCreate 内部 `clause.OnConflict{DoNothing: true}` 实现幂等
// - DB 里最终只保留 1 条记录(唯一约束生效)
//
// 设计意图cleanup_worker 重跑 / 多路径触发同一 (exhibition_id, like_id) 时,
// 重复记录会被静默忽略不影响主流程。RecordLikeBetRevenue 不需要 tolerate error。
db := setupTestDB(t)
defer cleanupTestRecords(t, db)
repo := NewLikeBetRevenueRepository(db)
now := time.Now().UnixMilli()
rec := &model.LikeBetRevenueRecord{
UserID: -30, StarID: 87, ExhibitionID: 90030, AssetID: 130,
LikeID: 8301, BetOrder: 1, TotalLikes: 1, CrystalAmount: 1,
CycleStartTime: now, CycleEndTime: now, Status: "claimable", CreatedAt: now,
}
if err := repo.BatchCreate([]*model.LikeBetRevenueRecord{rec}); err != nil {
t.Fatalf("First BatchCreate failed: %v", err)
}
// 重跑相同 (exhibition_id, like_id) —— 不应报错ON CONFLICT DO NOTHING 幂等)
rec2 := &model.LikeBetRevenueRecord{
UserID: -30, StarID: 87, ExhibitionID: 90030, AssetID: 130,
LikeID: 8301, BetOrder: 1, TotalLikes: 1, CrystalAmount: 1,
CycleStartTime: now, CycleEndTime: now, Status: "claimable", CreatedAt: now,
}
if err := repo.BatchCreate([]*model.LikeBetRevenueRecord{rec2}); err != nil {
t.Fatalf("Second BatchCreate should be no-op via ON CONFLICT DO NOTHING, got error: %v", err)
}
// 验证DB 里只能保留 1 条记录(唯一约束仍然生效)
var c int64
db.Model(&model.LikeBetRevenueRecord{}).Where("exhibition_id = ? AND like_id = ?", 90030, 8301).Count(&c)
if c != 1 {
t.Errorf("Expected 1 record (unique constraint enforces), got %d", c)
}
}