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>
375 lines
14 KiB
Go
375 lines
14 KiB
Go
package service
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/topfans/backend/pkg/database"
|
||
"github.com/topfans/backend/services/taskService/model"
|
||
"github.com/topfans/backend/services/taskService/repository"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// 注:本文件与 revenue_service_test.go 同包;如该包已有 TestMain 则复用,否则
|
||
// 共享 repository 包里的 TestMain(init logger + setup DB)。
|
||
//
|
||
// 本测试连接真实 dev PG(15432 / top-fans),复用 like_bet_repo_test.go 的 sentinel 模式。
|
||
|
||
// setupDailyTaskServiceTestDB 初始化 DB + AutoMigrate + 清掉旧 sentinel。
|
||
func setupDailyTaskServiceTestDB(t *testing.T) (*gorm.DB, repository.DailyTaskRepository) {
|
||
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.TaskDefinition{},
|
||
&model.UserDailyTaskProgress{},
|
||
); err != nil {
|
||
t.Logf("Warning: AutoMigrate: %v", err)
|
||
}
|
||
// 清掉之前测试的 sentinel(star_id=-77)
|
||
db.Where("star_id = ?", -77).Delete(&model.TaskDefinition{})
|
||
db.Where("user_id < ?", 0).Delete(&model.UserDailyTaskProgress{})
|
||
return db, repository.NewDailyTaskRepository(db)
|
||
}
|
||
|
||
// seedDef 插入一条 test daily 任务定义
|
||
func seedDef(t *testing.T, db *gorm.DB, triggerEvent string, targetCount int) *model.TaskDefinition {
|
||
def := &model.TaskDefinition{
|
||
StarID: nil,
|
||
TaskKey: "test_task_" + triggerEvent,
|
||
TaskType: "daily",
|
||
Name: "test daily " + triggerEvent,
|
||
CrystalReward: 10,
|
||
TriggerEvent: triggerEvent,
|
||
TargetCount: targetCount,
|
||
IsActive: true,
|
||
CreatedAt: time.Now().UnixMilli(),
|
||
UpdatedAt: time.Now().UnixMilli(),
|
||
}
|
||
if err := db.Create(def).Error; err != nil {
|
||
t.Fatalf("seed def: %v", err)
|
||
}
|
||
return def
|
||
}
|
||
|
||
// newTestDailyTaskService 不依赖 userRPCClient(ProcessTaskEvent 不需要)
|
||
func newTestDailyTaskService(repo repository.DailyTaskRepository) DailyTaskService {
|
||
return NewDailyTaskService(repo, nil)
|
||
}
|
||
|
||
// ==================== TestProcessTaskEvent_FirstCompletion ====================
|
||
|
||
func TestProcessTaskEvent_FirstCompletion_TargetOne(t *testing.T) {
|
||
db, repo := setupDailyTaskServiceTestDB(t)
|
||
defer db.Where("star_id = ?", -77).Delete(&model.TaskDefinition{})
|
||
|
||
def := seedDef(t, db, "test_first_complete", 1)
|
||
defer db.Where("id = ?", def.ID).Delete(&model.TaskDefinition{})
|
||
|
||
svc := newTestDailyTaskService(repo)
|
||
result, err := svc.ProcessTaskEvent(t.Context(), -200, -77, "test_first_complete")
|
||
if err != nil {
|
||
t.Fatalf("ProcessTaskEvent: %v", err)
|
||
}
|
||
|
||
if len(result.CompletedTaskKeys) != 1 {
|
||
t.Errorf("expected 1 completed, got %d: %v", len(result.CompletedTaskKeys), result.CompletedTaskKeys)
|
||
}
|
||
if result.CompletedTaskKeys[0] != def.TaskKey {
|
||
t.Errorf("expected TaskKey=%s, got %s", def.TaskKey, result.CompletedTaskKeys[0])
|
||
}
|
||
|
||
// 验证 DB
|
||
var progress model.UserDailyTaskProgress
|
||
if err := db.Where("user_id = ? AND task_key = ?", -200, def.TaskKey).First(&progress).Error; err != nil {
|
||
t.Fatalf("query progress: %v", err)
|
||
}
|
||
if progress.Status != "completed" {
|
||
t.Errorf("expected status=completed, got %s", progress.Status)
|
||
}
|
||
if progress.Progress != 1 {
|
||
t.Errorf("expected progress=1, got %d", progress.Progress)
|
||
}
|
||
if progress.CompletedAt == nil {
|
||
t.Errorf("expected completed_at set")
|
||
}
|
||
}
|
||
|
||
// ==================== TestProcessTaskEvent_Counting ====================
|
||
|
||
func TestProcessTaskEvent_Counting_AccumulatesToTarget(t *testing.T) {
|
||
db, repo := setupDailyTaskServiceTestDB(t)
|
||
defer db.Where("star_id = ?", -77).Delete(&model.TaskDefinition{})
|
||
|
||
def := seedDef(t, db, "test_counting", 3)
|
||
defer db.Where("id = ?", def.ID).Delete(&model.TaskDefinition{})
|
||
|
||
svc := newTestDailyTaskService(repo)
|
||
|
||
// 第 1 次:progress=1, status=pending
|
||
r1, err := svc.ProcessTaskEvent(t.Context(), -201, -77, "test_counting")
|
||
if err != nil {
|
||
t.Fatalf("emit 1: %v", err)
|
||
}
|
||
if len(r1.CompletedTaskKeys) != 0 {
|
||
t.Errorf("1st emit should not complete (target=3): %v", r1.CompletedTaskKeys)
|
||
}
|
||
|
||
// 第 2 次:progress=2
|
||
r2, err := svc.ProcessTaskEvent(t.Context(), -201, -77, "test_counting")
|
||
if err != nil {
|
||
t.Fatalf("emit 2: %v", err)
|
||
}
|
||
if len(r2.CompletedTaskKeys) != 0 {
|
||
t.Errorf("2nd emit should not complete: %v", r2.CompletedTaskKeys)
|
||
}
|
||
|
||
// 第 3 次:progress=3, completed
|
||
r3, err := svc.ProcessTaskEvent(t.Context(), -201, -77, "test_counting")
|
||
if err != nil {
|
||
t.Fatalf("emit 3: %v", err)
|
||
}
|
||
if len(r3.CompletedTaskKeys) != 1 || r3.CompletedTaskKeys[0] != def.TaskKey {
|
||
t.Errorf("3rd emit should complete: %v", r3.CompletedTaskKeys)
|
||
}
|
||
|
||
// 第 4 次:已 completed,no-op
|
||
r4, err := svc.ProcessTaskEvent(t.Context(), -201, -77, "test_counting")
|
||
if err != nil {
|
||
t.Fatalf("emit 4: %v", err)
|
||
}
|
||
if len(r4.CompletedTaskKeys) != 0 {
|
||
t.Errorf("4th emit should be no-op (already completed): %v", r4.CompletedTaskKeys)
|
||
}
|
||
|
||
// 验证最终 progress=3 (没有变成 4)
|
||
var progress model.UserDailyTaskProgress
|
||
db.Where("user_id = ? AND task_key = ?", -201, def.TaskKey).First(&progress)
|
||
if progress.Progress != 3 {
|
||
t.Errorf("expected Progress=3 after 4 emits (3rd should complete, 4th no-op), got %d", progress.Progress)
|
||
}
|
||
}
|
||
|
||
// ==================== TestProcessTaskEvent_AlreadyCompleted ====================
|
||
|
||
func TestProcessTaskEvent_AlreadyCompleted_Skipped(t *testing.T) {
|
||
db, repo := setupDailyTaskServiceTestDB(t)
|
||
defer db.Where("star_id = ?", -77).Delete(&model.TaskDefinition{})
|
||
|
||
def := seedDef(t, db, "test_already_completed", 1)
|
||
defer db.Where("id = ?", def.ID).Delete(&model.TaskDefinition{})
|
||
|
||
svc := newTestDailyTaskService(repo)
|
||
|
||
// 第一次:complete
|
||
_, err := svc.ProcessTaskEvent(t.Context(), -202, -77, "test_already_completed")
|
||
if err != nil {
|
||
t.Fatalf("first emit: %v", err)
|
||
}
|
||
|
||
// 第二次:已 completed → 跳过
|
||
r2, err := svc.ProcessTaskEvent(t.Context(), -202, -77, "test_already_completed")
|
||
if err != nil {
|
||
t.Fatalf("second emit: %v", err)
|
||
}
|
||
if len(r2.CompletedTaskKeys) != 0 {
|
||
t.Errorf("expected 0 completed (already completed), got %v", r2.CompletedTaskKeys)
|
||
}
|
||
}
|
||
|
||
// ==================== TestProcessTaskEvent_UnknownEvent ====================
|
||
|
||
func TestProcessTaskEvent_UnknownEvent_NoOp(t *testing.T) {
|
||
db, repo := setupDailyTaskServiceTestDB(t)
|
||
defer db.Where("star_id = ?", -77).Delete(&model.TaskDefinition{})
|
||
|
||
svc := newTestDailyTaskService(repo)
|
||
|
||
result, err := svc.ProcessTaskEvent(t.Context(), -203, -77, "test_unknown_event_xyz")
|
||
if err != nil {
|
||
t.Fatalf("unexpected error for unknown event: %v", err)
|
||
}
|
||
if len(result.CompletedTaskKeys) != 0 {
|
||
t.Errorf("expected 0 completed, got %v", result.CompletedTaskKeys)
|
||
}
|
||
// 不应创建 progress 行
|
||
var count int64
|
||
db.Model(&model.UserDailyTaskProgress{}).Where("user_id = ?", -203).Count(&count)
|
||
if count != 0 {
|
||
t.Errorf("expected no progress rows for unknown event, got %d", count)
|
||
}
|
||
}
|
||
|
||
// ==================== TestProcessTaskEvent_GlobalAndStarSpecific ====================
|
||
|
||
func TestProcessTaskEvent_GlobalAndStarSpecific_BothIncrement(t *testing.T) {
|
||
db, repo := setupDailyTaskServiceTestDB(t)
|
||
defer db.Where("star_id = ? OR star_id IS NULL", -77).Delete(&model.TaskDefinition{})
|
||
defer db.Where("user_id = ?", -204).Delete(&model.UserDailyTaskProgress{})
|
||
|
||
now := time.Now().UnixMilli()
|
||
starID := int64(87)
|
||
|
||
// 全局默认任务(star_id=NULL)
|
||
globalDef := &model.TaskDefinition{
|
||
TaskKey: "test_global_both", TaskType: "daily", Name: "global",
|
||
CrystalReward: 5, TriggerEvent: "test_both_event", TargetCount: 1,
|
||
IsActive: true, StarID: nil, CreatedAt: now, UpdatedAt: now,
|
||
}
|
||
if err := db.Create(globalDef).Error; err != nil {
|
||
t.Fatalf("seed global: %v", err)
|
||
}
|
||
defer db.Where("id = ?", globalDef.ID).Delete(&model.TaskDefinition{})
|
||
|
||
// star 专属任务(star_id=87)
|
||
starDef := &model.TaskDefinition{
|
||
TaskKey: "test_star_both", TaskType: "daily", Name: "star specific",
|
||
CrystalReward: 5, TriggerEvent: "test_both_event", TargetCount: 1,
|
||
IsActive: true, StarID: &starID, CreatedAt: now, UpdatedAt: now,
|
||
}
|
||
if err := db.Create(starDef).Error; err != nil {
|
||
t.Fatalf("seed star: %v", err)
|
||
}
|
||
defer db.Where("id = ?", starDef.ID).Delete(&model.TaskDefinition{})
|
||
|
||
svc := newTestDailyTaskService(repo)
|
||
result, err := svc.ProcessTaskEvent(t.Context(), -204, starID, "test_both_event")
|
||
if err != nil {
|
||
t.Fatalf("ProcessTaskEvent: %v", err)
|
||
}
|
||
|
||
if len(result.CompletedTaskKeys) != 2 {
|
||
t.Errorf("expected 2 completed (global + star specific), got %d: %v",
|
||
len(result.CompletedTaskKeys), result.CompletedTaskKeys)
|
||
}
|
||
|
||
// 验证 DB:两条 progress 都应该是 completed
|
||
var globalProgress, starProgress model.UserDailyTaskProgress
|
||
db.Where("user_id = ? AND task_key = ?", -204, globalDef.TaskKey).First(&globalProgress)
|
||
db.Where("user_id = ? AND task_key = ?", -204, starDef.TaskKey).First(&starProgress)
|
||
if globalProgress.Status != "completed" {
|
||
t.Errorf("global task not completed: %s", globalProgress.Status)
|
||
}
|
||
if starProgress.Status != "completed" {
|
||
t.Errorf("star task not completed: %s", starProgress.Status)
|
||
}
|
||
}
|
||
|
||
// TestReportEvent_UnknownEvent_ReturnsNoTaskCompleted 验证 ReportEvent 在没有匹配任务时
|
||
// 返回 Success=true(RPC 不报错)但 TaskCompleted=false(前端无奖励提示)
|
||
func TestReportEvent_UnknownEvent_ReturnsNoTaskCompleted(t *testing.T) {
|
||
db, repo := setupDailyTaskServiceTestDB(t)
|
||
defer db.Where("star_id = ?", -77).Delete(&model.TaskDefinition{})
|
||
defer db.Where("user_id = ?", -206).Delete(&model.UserDailyTaskProgress{})
|
||
|
||
svc := newTestDailyTaskService(repo)
|
||
resp, err := svc.ReportEvent(t.Context(), -206, -77, "test_unknown_event")
|
||
if err != nil {
|
||
t.Fatalf("ReportEvent: %v", err)
|
||
}
|
||
if !resp.Success {
|
||
t.Errorf("expected Success=true (no error), got false")
|
||
}
|
||
if resp.TaskCompleted {
|
||
t.Errorf("expected TaskCompleted=false (no matching task), got true")
|
||
}
|
||
if resp.TaskKey != "" {
|
||
t.Errorf("expected TaskKey=empty, got %s", resp.TaskKey)
|
||
}
|
||
}
|
||
|
||
// TestReportEvent_DelegatesToProcessTaskEvent 验证 ReportEvent 委托给 ProcessTaskEvent
|
||
// 并从 TaskEventResult 回填 TaskCompleted / TaskKey
|
||
func TestReportEvent_DelegatesToProcessTaskEvent(t *testing.T) {
|
||
db, repo := setupDailyTaskServiceTestDB(t)
|
||
defer db.Where("star_id = ?", -77).Delete(&model.TaskDefinition{})
|
||
defer db.Where("user_id = ?", -205).Delete(&model.UserDailyTaskProgress{})
|
||
|
||
def := seedDef(t, db, "test_report_event", 1)
|
||
defer db.Where("id = ?", def.ID).Delete(&model.TaskDefinition{})
|
||
|
||
svc := newTestDailyTaskService(repo)
|
||
resp, err := svc.ReportEvent(t.Context(), -205, -77, "test_report_event")
|
||
if err != nil {
|
||
t.Fatalf("ReportEvent: %v", err)
|
||
}
|
||
if !resp.TaskCompleted {
|
||
t.Errorf("expected TaskCompleted=true, got false (resp=%+v)", resp)
|
||
}
|
||
if resp.TaskKey != def.TaskKey {
|
||
t.Errorf("expected TaskKey=%s, got %s", def.TaskKey, resp.TaskKey)
|
||
}
|
||
}
|
||
|
||
// ==================== TestProcessTaskEvent_ErrorIsolation ====================
|
||
|
||
// TestProcessTaskEvent_SingleFailure_DoesNotBlockOthers
|
||
// 验证 spec §8:"引擎内单条任务更新失败不影响其他任务(逐个处理,记 error 日志)"
|
||
//
|
||
// 构造:两条 task_definitions trigger_event 相同(但 task_key 不同)。
|
||
// 第一条预先设 status='claimed' → IncrementProgress 行 0 影响(不会 +1)。
|
||
// 第二条保持 pending。
|
||
// 期望:第二条应该正常 +1 完成;CompletedTaskKeys 应只包含第二条 task_key。
|
||
func TestProcessTaskEvent_SingleFailure_DoesNotBlockOthers(t *testing.T) {
|
||
db, repo := setupDailyTaskServiceTestDB(t)
|
||
defer db.Where("star_id = ?", -77).Delete(&model.TaskDefinition{})
|
||
defer db.Where("user_id = ?", -207).Delete(&model.UserDailyTaskProgress{})
|
||
|
||
// seed 2 条相同 trigger_event 但 task_key 不同的定义
|
||
defA := seedDef(t, db, "test_isolation_event", 1)
|
||
defer db.Where("id = ?", defA.ID).Delete(&model.TaskDefinition{})
|
||
now := time.Now().UnixMilli()
|
||
defB := &model.TaskDefinition{
|
||
TaskKey: "test_taskB_isolation", TaskType: "daily", Name: "taskB",
|
||
CrystalReward: 5, TriggerEvent: "test_isolation_event", TargetCount: 1,
|
||
IsActive: true, StarID: nil, CreatedAt: now, UpdatedAt: now,
|
||
}
|
||
if err := db.Create(defB).Error; err != nil {
|
||
t.Fatalf("seed defB: %v", err)
|
||
}
|
||
defer db.Where("id = ?", defB.ID).Delete(&model.TaskDefinition{})
|
||
|
||
// 预先创建 defA 的 progress 行并强制 status='claimed'(模拟已领取,IncrementProgress 应被跳过)
|
||
preProgress := &model.UserDailyTaskProgress{
|
||
UserID: -207, StarID: -77, TaskKey: defA.TaskKey,
|
||
Status: "claimed", Progress: 1,
|
||
CreatedAt: now, UpdatedAt: now,
|
||
ClaimedAt: &now,
|
||
}
|
||
if err := db.Create(preProgress).Error; err != nil {
|
||
t.Fatalf("seed preProgress: %v", err)
|
||
}
|
||
defer db.Where("user_id = ?", -207).Delete(&model.UserDailyTaskProgress{})
|
||
|
||
svc := newTestDailyTaskService(repo)
|
||
result, err := svc.ProcessTaskEvent(t.Context(), -207, -77, "test_isolation_event")
|
||
if err != nil {
|
||
t.Fatalf("ProcessTaskEvent should not error (defA failure isolated): %v", err)
|
||
}
|
||
|
||
// 只应 completed 1 条(defB),defA 应被 skip
|
||
if len(result.CompletedTaskKeys) != 1 {
|
||
t.Fatalf("expected 1 completed (defB only), got %d: %v",
|
||
len(result.CompletedTaskKeys), result.CompletedTaskKeys)
|
||
}
|
||
if result.CompletedTaskKeys[0] != defB.TaskKey {
|
||
t.Errorf("expected completed taskKey=%s, got %s", defB.TaskKey, result.CompletedTaskKeys[0])
|
||
}
|
||
|
||
// 验证 defA 仍保持 claimed 状态(未被错误重置)
|
||
var defAProgress model.UserDailyTaskProgress
|
||
db.Where("user_id = ? AND task_key = ?", -207, defA.TaskKey).First(&defAProgress)
|
||
if defAProgress.Status != "claimed" {
|
||
t.Errorf("defA should still be claimed, got %s", defAProgress.Status)
|
||
}
|
||
} |