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