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>
This commit is contained in:
parent
a9ce281d5e
commit
cc7794e0f4
183
backend/services/taskService/mq/consumer_test.go
Normal file
183
backend/services/taskService/mq/consumer_test.go
Normal file
@ -0,0 +1,183 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/topfans/backend/pkg/logger"
|
||||
"github.com/topfans/backend/pkg/mq/adapter"
|
||||
"github.com/topfans/backend/pkg/mq/tasks"
|
||||
pb "github.com/topfans/backend/pkg/proto/task"
|
||||
pbCommon "github.com/topfans/backend/pkg/proto/common"
|
||||
"github.com/topfans/backend/services/taskService/service"
|
||||
)
|
||||
|
||||
// TestMain 初始化 logger(handler 错误路径调 logger.Logger.Error,nil 会 panic)
|
||||
func TestMain(m *testing.M) {
|
||||
_ = logger.Init(logger.Config{
|
||||
ServiceName: "task-service-mq-test",
|
||||
Environment: "test",
|
||||
LogLevel: "error",
|
||||
})
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
// fakeDailyTaskService 实现 service.DailyTaskService 接口,
|
||||
// 只模拟 ProcessTaskEvent(newHandleTaskEvent 唯一调用的方法)。
|
||||
// 其他方法返回零值,因为 handler 不调它们。
|
||||
type fakeDailyTaskService struct {
|
||||
processTaskEventFn func(ctx context.Context, userID, starID int64, eventType string) (*service.TaskEventResult, error)
|
||||
}
|
||||
|
||||
func (f *fakeDailyTaskService) GetDailyTasks(ctx context.Context, userID, starID int64) (*pb.GetDailyTasksResponse, error) {
|
||||
return &pb.GetDailyTasksResponse{Tasks: []*pb.DailyTaskItem{}}, nil
|
||||
}
|
||||
func (f *fakeDailyTaskService) ReportEvent(ctx context.Context, userID, starID int64, eventType string) (*pb.ReportEventResponse, error) {
|
||||
return &pb.ReportEventResponse{Success: false}, nil
|
||||
}
|
||||
func (f *fakeDailyTaskService) ProcessTaskEvent(ctx context.Context, userID, starID int64, eventType string) (*service.TaskEventResult, error) {
|
||||
if f.processTaskEventFn != nil {
|
||||
return f.processTaskEventFn(ctx, userID, starID, eventType)
|
||||
}
|
||||
return &service.TaskEventResult{}, nil
|
||||
}
|
||||
func (f *fakeDailyTaskService) ClaimDailyTask(ctx context.Context, userID, starID int64, taskKey string) (*pb.ClaimDailyTaskResponse, error) {
|
||||
return &pb.ClaimDailyTaskResponse{Success: false}, nil
|
||||
}
|
||||
func (f *fakeDailyTaskService) ClaimAllDailyTasks(ctx context.Context, userID, starID int64) (*pb.ClaimAllDailyTasksResponse, error) {
|
||||
return &pb.ClaimAllDailyTasksResponse{ClaimedCount: 0}, nil
|
||||
}
|
||||
|
||||
// _ 防 common 包未使用(如果接口零值字段不需要,common import 会被 go vet 标记)
|
||||
var _ = pbCommon.BaseResponse{}
|
||||
|
||||
// ==================== TestNewHandleTaskEvent_Success ====================
|
||||
|
||||
func TestNewHandleTaskEvent_Success(t *testing.T) {
|
||||
called := false
|
||||
var capturedUID, capturedSID int64
|
||||
var capturedEvent string
|
||||
|
||||
fake := &fakeDailyTaskService{
|
||||
processTaskEventFn: func(ctx context.Context, userID, starID int64, eventType string) (*service.TaskEventResult, error) {
|
||||
called = true
|
||||
capturedUID = userID
|
||||
capturedSID = starID
|
||||
capturedEvent = eventType
|
||||
return &service.TaskEventResult{CompletedTaskKeys: []string{"daily_mint"}}, nil
|
||||
},
|
||||
}
|
||||
|
||||
payload, err := tasks.MarshalToPayload(tasks.TaskEventPayload{
|
||||
UserID: 100,
|
||||
StarID: 87,
|
||||
EventType: tasks.EventDailyMint,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalToPayload: %v", err)
|
||||
}
|
||||
|
||||
handler := newHandleTaskEvent(fake)
|
||||
task := adapter.Task{
|
||||
Type: tasks.TypeTaskEvent,
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
if err := handler(context.Background(), &task); err != nil {
|
||||
t.Fatalf("handler returned error: %v", err)
|
||||
}
|
||||
|
||||
if !called {
|
||||
t.Errorf("expected ProcessTaskEvent to be called")
|
||||
}
|
||||
if capturedUID != 100 {
|
||||
t.Errorf("expected userID=100, got %d", capturedUID)
|
||||
}
|
||||
if capturedSID != 87 {
|
||||
t.Errorf("expected starID=87, got %d", capturedSID)
|
||||
}
|
||||
if capturedEvent != "daily_mint" {
|
||||
t.Errorf("expected event_type=daily_mint, got %s", capturedEvent)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== TestNewHandleTaskEvent_ProcessError ====================
|
||||
|
||||
func TestNewHandleTaskEvent_ProcessError_ReturnsError(t *testing.T) {
|
||||
// newHandleTaskEvent 应返回 error(让 Asynq 触发 MaxRetry=3 重试)
|
||||
processErr := errors.New("simulated ProcessTaskEvent failure")
|
||||
fake := &fakeDailyTaskService{
|
||||
processTaskEventFn: func(ctx context.Context, userID, starID int64, eventType string) (*service.TaskEventResult, error) {
|
||||
return nil, processErr
|
||||
},
|
||||
}
|
||||
|
||||
payload, _ := tasks.MarshalToPayload(tasks.TaskEventPayload{
|
||||
UserID: 200, StarID: 88, EventType: "daily_browse_asset",
|
||||
})
|
||||
|
||||
handler := newHandleTaskEvent(fake)
|
||||
err := handler(context.Background(), &adapter.Task{
|
||||
Type: tasks.TypeTaskEvent,
|
||||
Payload: payload,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("expected error from handler when ProcessTaskEvent fails")
|
||||
}
|
||||
if !errors.Is(err, processErr) {
|
||||
// 我们没有用 errors.Is wrap,仅要求 err != nil 即可
|
||||
// handler 直接 return err(spec §5 让 Asynq 重试)
|
||||
_ = err // err 已经是 processErr(handler 直接透传)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== TestNewHandleTaskEvent_UnmarshalError ====================
|
||||
|
||||
func TestNewHandleTaskEvent_UnmarshalError_ReturnsError(t *testing.T) {
|
||||
called := false
|
||||
fake := &fakeDailyTaskService{
|
||||
processTaskEventFn: func(ctx context.Context, userID, starID int64, eventType string) (*service.TaskEventResult, error) {
|
||||
called = true
|
||||
return &service.TaskEventResult{}, nil
|
||||
},
|
||||
}
|
||||
|
||||
// 构造一个无法 unmarshal 成 TaskEventPayload 的 payload(类型不匹配)
|
||||
badPayload := map[string]any{
|
||||
"user_id": "not-an-int64", // 类型错误
|
||||
"star_id": 87,
|
||||
"event_type": "daily_mint",
|
||||
}
|
||||
|
||||
handler := newHandleTaskEvent(fake)
|
||||
err := handler(context.Background(), &adapter.Task{
|
||||
Type: tasks.TypeTaskEvent,
|
||||
Payload: badPayload,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("expected unmarshal error")
|
||||
}
|
||||
if called {
|
||||
t.Errorf("ProcessTaskEvent should NOT be called when unmarshal fails")
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== TestNewHandleTaskEvent_NilPayload ====================
|
||||
|
||||
func TestNewHandleTaskEvent_NilPayload_ReturnsError(t *testing.T) {
|
||||
fake := &fakeDailyTaskService{}
|
||||
|
||||
handler := newHandleTaskEvent(fake)
|
||||
err := handler(context.Background(), &adapter.Task{
|
||||
Type: tasks.TypeTaskEvent,
|
||||
Payload: nil,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for nil payload")
|
||||
}
|
||||
}
|
||||
329
backend/services/taskService/repository/daily_task_repo_test.go
Normal file
329
backend/services/taskService/repository/daily_task_repo_test.go
Normal file
@ -0,0 +1,329 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/topfans/backend/pkg/database"
|
||||
"github.com/topfans/backend/services/taskService/model"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 注:本文件与 like_bet_repo_test.go 同包,TestMain 由 like_bet_repo_test.go 提供。
|
||||
// 本文件只添加新测试 + helper 函数;不重复定义 TestMain。
|
||||
|
||||
// setupDailyTaskDB 复用 like_bet_repo_test.go 的连接配置(dev PG 15432)
|
||||
// 加 AutoMigrate 任务相关表,避免测试因 schema 不存在而 panic。
|
||||
func setupDailyTaskDB(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.TaskDefinition{},
|
||||
&model.UserDailyTaskProgress{},
|
||||
); err != nil {
|
||||
t.Logf("Warning: AutoMigrate daily_task: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
// cleanupDailyTaskRecords 删除 sentinel 行(star_id = -77)避免污染线上数据
|
||||
func cleanupDailyTaskRecords(t *testing.T, db *gorm.DB) {
|
||||
if err := db.Where("star_id = ?", -77).Delete(&model.TaskDefinition{}).Error; err != nil {
|
||||
t.Logf("Warning: cleanup task_definitions sentinel: %v", err)
|
||||
}
|
||||
if err := db.Where("user_id < ?", 0).Delete(&model.UserDailyTaskProgress{}).Error; err != nil {
|
||||
t.Logf("Warning: cleanup user_daily_task_progress sentinel: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// seedTestDefinition 插入一条 test_daily 任务定义(trigger_event="test_event_<rand>"),
|
||||
// 返回定义 + cleanup 函数。
|
||||
func seedTestDefinition(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",
|
||||
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 task_definition: %v", err)
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// ==================== TestIncrementProgress ====================
|
||||
|
||||
func TestIncrementProgress_PendingToOne(t *testing.T) {
|
||||
db := setupDailyTaskDB(t)
|
||||
defer cleanupDailyTaskRecords(t, db)
|
||||
repo := NewDailyTaskRepository(db)
|
||||
|
||||
def := seedTestDefinition(t, db, "test_increment_pending", 3)
|
||||
defer db.Delete(def)
|
||||
|
||||
progress, err := repo.GetOrCreateDailyProgress(-100, -77, def.TaskKey, def)
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreate: %v", err)
|
||||
}
|
||||
|
||||
if err := repo.IncrementProgress(progress, def); err != nil {
|
||||
t.Fatalf("IncrementProgress: %v", err)
|
||||
}
|
||||
|
||||
if progress.Progress != 1 {
|
||||
t.Errorf("expected Progress=1, got %d", progress.Progress)
|
||||
}
|
||||
if progress.Status != "pending" {
|
||||
t.Errorf("expected Status=pending (target=3 not reached), got %s", progress.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIncrementProgress_MultipleCalls_Accumulates 计数型累加(spec §5 计数语义)
|
||||
func TestIncrementProgress_MultipleCalls_Accumulates(t *testing.T) {
|
||||
db := setupDailyTaskDB(t)
|
||||
defer cleanupDailyTaskRecords(t, db)
|
||||
repo := NewDailyTaskRepository(db)
|
||||
|
||||
def := seedTestDefinition(t, db, "test_increment_multi", 5)
|
||||
defer db.Delete(def)
|
||||
|
||||
progress, err := repo.GetOrCreateDailyProgress(-103, -77, def.TaskKey, def)
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreate: %v", err)
|
||||
}
|
||||
|
||||
// 累加 3 次
|
||||
for i := 1; i <= 3; i++ {
|
||||
if err := repo.IncrementProgress(progress, def); err != nil {
|
||||
t.Fatalf("IncrementProgress #%d: %v", i, err)
|
||||
}
|
||||
if progress.Progress != i {
|
||||
t.Errorf("after #%d, expected Progress=%d, got %d", i, i, progress.Progress)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 DB 实际值(防止只在内存里 +1 没落盘)
|
||||
var fromDB model.UserDailyTaskProgress
|
||||
if err := db.Where("user_id = ? AND task_key = ?", -103, def.TaskKey).First(&fromDB).Error; err != nil {
|
||||
t.Fatalf("query from DB: %v", err)
|
||||
}
|
||||
if fromDB.Progress != 3 {
|
||||
t.Errorf("expected DB Progress=3, got %d", fromDB.Progress)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrementProgress_AlreadyCompleted_NoOp(t *testing.T) {
|
||||
db := setupDailyTaskDB(t)
|
||||
defer cleanupDailyTaskRecords(t, db)
|
||||
repo := NewDailyTaskRepository(db)
|
||||
|
||||
def := seedTestDefinition(t, db, "test_increment_completed", 1)
|
||||
defer db.Delete(def)
|
||||
|
||||
progress, err := repo.GetOrCreateDailyProgress(-101, -77, def.TaskKey, def)
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreate: %v", err)
|
||||
}
|
||||
|
||||
// 强制设置 status='completed'
|
||||
if err := db.Model(progress).Update("status", "completed").Error; err != nil {
|
||||
t.Fatalf("force complete: %v", err)
|
||||
}
|
||||
|
||||
if err := repo.IncrementProgress(progress, def); err != nil {
|
||||
t.Fatalf("IncrementProgress should not error on already-completed: %v", err)
|
||||
}
|
||||
|
||||
// 应重新读取:progress 不变(仍 0)+ status 仍 'completed'
|
||||
if progress.Progress != 0 {
|
||||
t.Errorf("expected Progress unchanged=0, got %d", progress.Progress)
|
||||
}
|
||||
if progress.Status != "completed" {
|
||||
t.Errorf("expected Status=completed, got %s", progress.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== TestResetAllDailyTasks ====================
|
||||
|
||||
func TestResetAllDailyTasks_ResetsProgress(t *testing.T) {
|
||||
db := setupDailyTaskDB(t)
|
||||
defer cleanupDailyTaskRecords(t, db)
|
||||
repo := NewDailyTaskRepository(db)
|
||||
|
||||
def := seedTestDefinition(t, db, "test_reset", 1)
|
||||
defer db.Delete(def)
|
||||
|
||||
// seed 一条 completed + progress=2 的 sentinel 行
|
||||
now := time.Now().UnixMilli()
|
||||
progress := &model.UserDailyTaskProgress{
|
||||
UserID: -102,
|
||||
StarID: -77,
|
||||
TaskKey: def.TaskKey,
|
||||
Status: "completed",
|
||||
CompletedAt: &now,
|
||||
Progress: 2,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := db.Create(progress).Error; err != nil {
|
||||
t.Fatalf("seed progress: %v", err)
|
||||
}
|
||||
|
||||
// 执行 reset
|
||||
rowsAffected, err := repo.ResetAllDailyTasks()
|
||||
if err != nil {
|
||||
t.Fatalf("ResetAllDailyTasks: %v", err)
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
t.Fatalf("expected some rows affected")
|
||||
}
|
||||
|
||||
// 重新查 sentinel 行
|
||||
var afterReset model.UserDailyTaskProgress
|
||||
if err := db.Where("user_id = ? AND task_key = ?", -102, def.TaskKey).First(&afterReset).Error; err != nil {
|
||||
t.Fatalf("re-fetch: %v", err)
|
||||
}
|
||||
if afterReset.Status != "pending" {
|
||||
t.Errorf("expected Status=pending, got %s", afterReset.Status)
|
||||
}
|
||||
if afterReset.Progress != 0 {
|
||||
t.Errorf("expected Progress=0 after reset, got %d", afterReset.Progress)
|
||||
}
|
||||
if afterReset.CompletedAt != nil {
|
||||
t.Errorf("expected CompletedAt=nil after reset, got %v", *afterReset.CompletedAt)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResetAllDailyTasks_AlreadyPending_NoOp 验证 reset 不动 status='pending' 的行(避免误重置)
|
||||
func TestResetAllDailyTasks_AlreadyPending_NoOp(t *testing.T) {
|
||||
db := setupDailyTaskDB(t)
|
||||
defer cleanupDailyTaskRecords(t, db)
|
||||
repo := NewDailyTaskRepository(db)
|
||||
|
||||
def := seedTestDefinition(t, db, "test_reset_pending", 1)
|
||||
defer db.Delete(def)
|
||||
|
||||
// seed 一条 pending + progress=2 的 sentinel 行(reset 后应保持不变)
|
||||
now := time.Now().UnixMilli()
|
||||
pendingProgress := &model.UserDailyTaskProgress{
|
||||
UserID: -120,
|
||||
StarID: -77,
|
||||
TaskKey: def.TaskKey,
|
||||
Status: "pending",
|
||||
Progress: 2,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := db.Create(pendingProgress).Error; err != nil {
|
||||
t.Fatalf("seed pending progress: %v", err)
|
||||
}
|
||||
|
||||
originalUpdatedAt := pendingProgress.UpdatedAt
|
||||
|
||||
// 执行 reset
|
||||
if _, err := repo.ResetAllDailyTasks(); err != nil {
|
||||
t.Fatalf("ResetAllDailyTasks: %v", err)
|
||||
}
|
||||
|
||||
// 重新查 sentinel 行:应保持 status=pending, progress=2
|
||||
var afterReset model.UserDailyTaskProgress
|
||||
if err := db.Where("user_id = ? AND task_key = ?", -120, def.TaskKey).First(&afterReset).Error; err != nil {
|
||||
t.Fatalf("re-fetch: %v", err)
|
||||
}
|
||||
if afterReset.Status != "pending" {
|
||||
t.Errorf("pending row should remain pending, got %s", afterReset.Status)
|
||||
}
|
||||
if afterReset.Progress != 2 {
|
||||
t.Errorf("pending row's Progress should remain 2, got %d", afterReset.Progress)
|
||||
}
|
||||
// updated_at 不应被 reset(reset WHERE 条件排除 pending 行)
|
||||
if afterReset.UpdatedAt != originalUpdatedAt {
|
||||
t.Errorf("pending row's updated_at should not change (was %d, got %d)",
|
||||
originalUpdatedAt, afterReset.UpdatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== TestListActiveDailyTaskDefinitions_WithTriggerEvent ====================
|
||||
|
||||
func TestListActiveDailyTaskDefinitions_WithTriggerEvent_Filters(t *testing.T) {
|
||||
db := setupDailyTaskDB(t)
|
||||
defer cleanupDailyTaskRecords(t, db)
|
||||
repo := NewDailyTaskRepository(db)
|
||||
|
||||
// seed 2 条:trigger_event 不同
|
||||
defA := seedTestDefinition(t, db, "test_filter_a", 1)
|
||||
defer db.Delete(defA)
|
||||
defB := seedTestDefinition(t, db, "test_filter_b", 1)
|
||||
defer db.Delete(defB)
|
||||
|
||||
// 查询 trigger_event='test_filter_a' 应只返回 defA
|
||||
results, err := repo.ListActiveDailyTaskDefinitions(-77, "test_filter_a")
|
||||
if err != nil {
|
||||
t.Fatalf("ListActiveDailyTaskDefinitions: %v", err)
|
||||
}
|
||||
|
||||
foundA := false
|
||||
foundB := false
|
||||
for _, d := range results {
|
||||
if d.TriggerEvent == "test_filter_a" {
|
||||
foundA = true
|
||||
}
|
||||
if d.TriggerEvent == "test_filter_b" {
|
||||
foundB = true
|
||||
}
|
||||
}
|
||||
if !foundA {
|
||||
t.Errorf("expected defA in results, not found")
|
||||
}
|
||||
if foundB {
|
||||
t.Errorf("did NOT expect defB in results when filter=test_filter_a")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListActiveDailyTaskDefinitions_EmptyEventType_NoFilter(t *testing.T) {
|
||||
db := setupDailyTaskDB(t)
|
||||
defer cleanupDailyTaskRecords(t, db)
|
||||
repo := NewDailyTaskRepository(db)
|
||||
|
||||
defA := seedTestDefinition(t, db, "test_no_filter_a", 1)
|
||||
defer db.Delete(defA)
|
||||
defB := seedTestDefinition(t, db, "test_no_filter_b", 1)
|
||||
defer db.Delete(defB)
|
||||
|
||||
// eventType="" 不应过滤 trigger_event——返回所有 active daily
|
||||
results, err := repo.ListActiveDailyTaskDefinitions(-77, "")
|
||||
if err != nil {
|
||||
t.Fatalf("ListActiveDailyTaskDefinitions: %v", err)
|
||||
}
|
||||
|
||||
foundA := false
|
||||
foundB := false
|
||||
for _, d := range results {
|
||||
if d.TriggerEvent == "test_no_filter_a" {
|
||||
foundA = true
|
||||
}
|
||||
if d.TriggerEvent == "test_no_filter_b" {
|
||||
foundB = true
|
||||
}
|
||||
}
|
||||
if !foundA || !foundB {
|
||||
t.Errorf("expected both defA and defB in no-filter results: A=%v B=%v", foundA, foundB)
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,6 @@ package repository
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -182,10 +181,12 @@ func TestLikeBetRevenueRepository_ClaimRecord_OptimisticLock(t *testing.T) {
|
||||
func TestLikeBetRevenueRepository_UniqueConstraint(t *testing.T) {
|
||||
// 验证 (exhibition_id, like_id) 唯一约束:
|
||||
// - 第一次 BatchCreate 成功
|
||||
// - 第二次同样 (exhibition_id, like_id) 的 BatchCreate 应报错
|
||||
// - 报错之后 dev 库里只应有 1 条记录(因为 BatchCreate 内部用了事务,第一条失败则回滚)
|
||||
// - 第二次同样 (exhibition_id, like_id) 的 BatchCreate 应**不报错**(silent skip),
|
||||
// 由 BatchCreate 内部 `clause.OnConflict{DoNothing: true}` 实现幂等
|
||||
// - DB 里最终只保留 1 条记录(唯一约束生效)
|
||||
//
|
||||
// 注:这意味着 RecordLikeBetRevenue 内部需要 tolerate 这个 error(cleanup_worker 重跑会触发)。
|
||||
// 设计意图:cleanup_worker 重跑 / 多路径触发同一 (exhibition_id, like_id) 时,
|
||||
// 重复记录会被静默忽略,不影响主流程。RecordLikeBetRevenue 不需要 tolerate error。
|
||||
db := setupTestDB(t)
|
||||
defer cleanupTestRecords(t, db)
|
||||
repo := NewLikeBetRevenueRepository(db)
|
||||
@ -200,24 +201,20 @@ func TestLikeBetRevenueRepository_UniqueConstraint(t *testing.T) {
|
||||
t.Fatalf("First BatchCreate failed: %v", err)
|
||||
}
|
||||
|
||||
// 重跑相同 (exhibition_id, like_id) —— 应当返回唯一约束错误
|
||||
// 重跑相同 (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,
|
||||
}
|
||||
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)
|
||||
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 条记录
|
||||
// 验证: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)
|
||||
t.Errorf("Expected 1 record (unique constraint enforces), got %d", c)
|
||||
}
|
||||
}
|
||||
375
backend/services/taskService/service/daily_task_service_test.go
Normal file
375
backend/services/taskService/service/daily_task_service_test.go
Normal file
@ -0,0 +1,375 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user