package repository import ( "os" "strings" "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 应报错 // - 报错之后 dev 库里只应有 1 条记录(因为 BatchCreate 内部用了事务,第一条失败则回滚) // // 注:这意味着 RecordLikeBetRevenue 内部需要 tolerate 这个 error(cleanup_worker 重跑会触发)。 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) —— 应当返回唯一约束错误 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, } err := repo.BatchCreate([]*model.LikeBetRevenueRecord{rec2}) if err == nil { t.Fatal("Second BatchCreate should fail with unique constraint violation") } if !strings.Contains(err.Error(), "uk_like_bet_unique") { t.Errorf("Expected uk_like_bet_unique error, got: %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, got %d (unique constraint broken?)", c) } }