package taskcenter import ( "context" "testing" "chatapp3-golang/internal/common" "chatapp3-golang/internal/config" "chatapp3-golang/internal/integration" "chatapp3-golang/internal/model" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func newTestService(t *testing.T, java taskCenterJavaGateway) (*Service, *gorm.DB) { t.Helper() db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate( &model.TaskCenterConfig{}, &model.TaskCenterTaskConfig{}, &model.TaskCenterConditionJumpConfig{}, &model.TaskCenterUserProgress{}, &model.TaskCenterEvent{}, &model.TaskCenterClaimRecord{}, ); err != nil { t.Fatalf("auto migrate: %v", err) } service := NewService(config.Config{ TaskCenter: config.TaskCenterConfig{ DefaultSysOrigin: "LIKEI", Timezone: "Asia/Riyadh", }, }, db, java) return service, db } func TestDailyTaskConfigEventListAndClaim(t *testing.T) { java := &taskCenterTestGateway{events: map[string]bool{}} service, _ := newTestService(t, java) ctx := context.Background() configResp, err := service.SaveConfig(ctx, SaveConfigRequest{ SysOrigin: "likei", Enabled: true, Timezone: "Asia/Riyadh", TaskCategory: TaskCategoryDaily, Tasks: []SaveTaskConfigInput{ { TaskCode: "daily_mic_duration", Enabled: true, TaskName: "使用麦克风 %d 分钟。", TaskDesc: "进度 / 目标", ConditionType: "mic_duration_seconds", TargetValue: 60, TargetUnit: "second", RewardGold: 1100, JumpType: "ROOM_RANDOM_WITH_MIC", SortOrder: 10, }, }, }) if err != nil { t.Fatalf("SaveConfig() error = %v", err) } if !configResp.Configured || !configResp.Enabled || len(configResp.Tasks) != 1 { t.Fatalf("SaveConfig() resp = %+v", configResp) } user := common.AuthUser{UserID: 1001, SysOrigin: "LIKEI"} tasks, err := service.ListTasks(ctx, user) if err != nil { t.Fatalf("ListTasks() error = %v", err) } if len(tasks) != 1 || tasks[0].TaskStatus != 0 || tasks[0].CompletedValue != 0 { t.Fatalf("initial tasks = %+v", tasks) } eventResp, err := service.ReceiveEvent(ctx, EventRequest{ SysOrigin: "LIKEI", EventID: "event-1", EventType: "MIC_DURATION_SECONDS", UserID: flexibleInt64(user.UserID), DeltaValue: 60, }) if err != nil { t.Fatalf("ReceiveEvent() error = %v", err) } if !eventResp.Processed || eventResp.Updated != 1 { t.Fatalf("ReceiveEvent() resp = %+v", eventResp) } tasks, err = service.ListTasks(ctx, user) if err != nil { t.Fatalf("ListTasks() after event error = %v", err) } if len(tasks) != 1 || tasks[0].TaskStatus != 1 || tasks[0].CompletedValue != 60 { t.Fatalf("completed tasks = %+v", tasks) } ok, err := service.Claim(ctx, user, tasks[0].TaskID) if err != nil { t.Fatalf("Claim() error = %v", err) } if !ok { t.Fatalf("Claim() = false, want true") } if len(java.changedEvents) != 1 { t.Fatalf("wallet changes = %d, want 1", len(java.changedEvents)) } if java.changedCommands[0].Origin != "DAILY_TASK" { t.Fatalf("wallet origin = %q, want DAILY_TASK", java.changedCommands[0].Origin) } if len(java.changedEvents[0]) > 50 { t.Fatalf("wallet event id length = %d, want <= 50: %s", len(java.changedEvents[0]), java.changedEvents[0]) } ok, err = service.Claim(ctx, user, tasks[0].TaskID) if err != nil { t.Fatalf("Claim() duplicate error = %v", err) } if !ok || len(java.changedEvents) != 1 { t.Fatalf("duplicate claim ok=%v changes=%d, want true/1", ok, len(java.changedEvents)) } } func TestExclusiveTaskConfigUsesLifetimeCycleAndHidesAfterClaim(t *testing.T) { java := &taskCenterTestGateway{events: map[string]bool{}} service, _ := newTestService(t, java) ctx := context.Background() _, err := service.SaveConfig(ctx, SaveConfigRequest{ SysOrigin: "LIKEI", Enabled: true, Timezone: "Asia/Riyadh", TaskCategory: TaskCategoryNewbie, Tasks: []SaveTaskConfigInput{ { TaskCode: "EXCLUSIVE_RECHARGE", Enabled: true, TaskName: "专属充值任务", ConditionType: "RECHARGE_GOLD", TargetValue: 10, TargetUnit: "gold", RewardGold: 100, }, }, }) if err != nil { t.Fatalf("SaveConfig() error = %v", err) } user := common.AuthUser{UserID: 2001, SysOrigin: "LIKEI"} tasks, err := service.ListTasks(ctx, user) if err != nil { t.Fatalf("ListTasks() error = %v", err) } if len(tasks) != 1 || tasks[0].TaskType != 1 { t.Fatalf("exclusive tasks = %+v, want one taskType=1", tasks) } if tasks[0].JumpPage != defaultRechargeJumpPage { t.Fatalf("exclusive recharge jumpPage = %q, want %q", tasks[0].JumpPage, defaultRechargeJumpPage) } eventResp, err := service.ReceiveEvent(ctx, EventRequest{ SysOrigin: "LIKEI", EventID: "recharge-1", EventType: "RECHARGE_GOLD", UserID: flexibleInt64(user.UserID), DeltaValue: 10, }) if err != nil { t.Fatalf("ReceiveEvent() error = %v", err) } if !eventResp.Processed || eventResp.Updated != 1 { t.Fatalf("ReceiveEvent() resp = %+v", eventResp) } ok, err := service.Claim(ctx, user, tasks[0].TaskID) if err != nil { t.Fatalf("Claim() error = %v", err) } if !ok { t.Fatalf("Claim() = false, want true") } tasks, err = service.ListTasks(ctx, user) if err != nil { t.Fatalf("ListTasks() after claim error = %v", err) } if len(tasks) != 0 { t.Fatalf("exclusive tasks after claim = %+v, want hidden", tasks) } } func TestConditionJumpConfigAppliesToTasks(t *testing.T) { service, _ := newTestService(t, nil) ctx := context.Background() resp, err := service.SaveConfig(ctx, SaveConfigRequest{ SysOrigin: "LIKEI", Enabled: true, Timezone: "Asia/Riyadh", TaskCategory: TaskCategoryDaily, ConditionJumps: []SaveConditionJumpConfigInput{ { ConditionType: "MIC_DURATION_SECONDS", JumpType: "APP_ROUTE", JumpPage: "/task/mic-guide", SortOrder: 10, }, { ConditionType: "GIFT_CONSUME_GOLD", JumpType: "ROOM_RANDOM", JumpPage: "/room?source=custom", SortOrder: 20, }, }, Tasks: []SaveTaskConfigInput{ { TaskCode: "DAILY_MIC", Enabled: true, TaskName: "Mic task", ConditionType: "MIC_DURATION_SECONDS", TargetValue: 60, TargetUnit: "second", RewardGold: 100, JumpType: "ROOM_RANDOM_WITH_MIC", SortOrder: 10, }, { TaskCode: "DAILY_GIFT", Enabled: true, TaskName: "Gift task", ConditionType: "GIFT_CONSUME_GOLD", TargetValue: 1, TargetUnit: "gold", RewardGold: 100, SortOrder: 20, }, }, }) if err != nil { t.Fatalf("SaveConfig() error = %v", err) } if len(resp.ConditionJumps) != 2 { t.Fatalf("condition jumps = %+v, want 2 rows", resp.ConditionJumps) } if resp.ConditionJumps[1].JumpPage != "/room?source=custom" { t.Fatalf("room condition jump page = %q, want preserved", resp.ConditionJumps[1].JumpPage) } if resp.Tasks[0].JumpType != "APP_ROUTE" || resp.Tasks[0].JumpPage != "/task/mic-guide" { t.Fatalf("mic task jump = %s %s, want APP_ROUTE /task/mic-guide", resp.Tasks[0].JumpType, resp.Tasks[0].JumpPage) } if resp.Tasks[1].JumpType != "ROOM_RANDOM" || resp.Tasks[1].JumpPage != "/room?source=custom" { t.Fatalf("gift task jump = %s %s, want ROOM_RANDOM /room?source=custom", resp.Tasks[1].JumpType, resp.Tasks[1].JumpPage) } tasks, err := service.ListTasks(ctx, common.AuthUser{UserID: 3001, SysOrigin: "LIKEI"}) if err != nil { t.Fatalf("ListTasks() error = %v", err) } if len(tasks) != 2 || tasks[0].JumpPage != "/task/mic-guide" || tasks[1].JumpPage != "/room?source=custom" { t.Fatalf("app task jumps = %+v, want custom route and custom room param", tasks) } } func TestSaveConfigDerivesConditionJumpsWhenPayloadOmitted(t *testing.T) { service, _ := newTestService(t, nil) ctx := context.Background() resp, err := service.SaveConfig(ctx, SaveConfigRequest{ SysOrigin: "LIKEI", Enabled: true, Timezone: "Asia/Riyadh", TaskCategory: TaskCategoryDaily, Tasks: []SaveTaskConfigInput{ { TaskCode: "DAILY_MIC", Enabled: true, TaskName: "Mic task", ConditionType: "MIC_DURATION_SECONDS", TargetValue: 60, TargetUnit: "second", RewardGold: 100, JumpType: "APP_ROUTE", JumpPage: "/task/mic-guide", SortOrder: 10, }, { TaskCode: "DAILY_GIFT", Enabled: true, TaskName: "Gift task", ConditionType: "GIFT_CONSUME_GOLD", TargetValue: 1, TargetUnit: "gold", RewardGold: 100, JumpType: "ROOM_RANDOM", JumpPage: "/room?source=custom", SortOrder: 20, }, }, }) if err != nil { t.Fatalf("SaveConfig() error = %v", err) } if len(resp.ConditionJumps) != 2 { t.Fatalf("condition jumps = %+v, want derived rows", resp.ConditionJumps) } if resp.ConditionJumps[0].JumpPage != "/task/mic-guide" || resp.ConditionJumps[1].JumpPage != "/room?source=custom" { t.Fatalf("condition jump pages = %+v, want task jump fields persisted", resp.ConditionJumps) } if resp.Tasks[0].JumpType != "APP_ROUTE" || resp.Tasks[0].JumpPage != "/task/mic-guide" { t.Fatalf("mic task jump = %s %s, want APP_ROUTE /task/mic-guide", resp.Tasks[0].JumpType, resp.Tasks[0].JumpPage) } } func TestResolveJumpPageUsesRechargeRouteForRechargeTask(t *testing.T) { tests := []struct { name string task model.TaskCenterTaskConfig want string }{ { name: "recharge task with old room jump type", task: model.TaskCenterTaskConfig{ ConditionType: EventTypeRechargeGold, JumpType: "ROOM_RANDOM_WITH_MIC", }, want: defaultRechargeJumpPage, }, { name: "recharge jump type", task: model.TaskCenterTaskConfig{ JumpType: JumpTypeRechargeInApp, }, want: defaultRechargeJumpPage, }, { name: "explicit jump page wins", task: model.TaskCenterTaskConfig{ ConditionType: EventTypeRechargeGold, JumpPage: "/custom/recharge", JumpType: "ROOM_RANDOM_WITH_MIC", }, want: "/custom/recharge", }, { name: "room task keeps room jump", task: model.TaskCenterTaskConfig{ ConditionType: EventTypeMicDurationSeconds, JumpType: "ROOM_RANDOM_WITH_MIC", }, want: "/room", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := resolveJumpPage(tt.task); got != tt.want { t.Fatalf("resolveJumpPage() = %q, want %q", got, tt.want) } }) } } func TestTaskConfigRejectsRemovedConditionTypes(t *testing.T) { service, _ := newTestService(t, nil) for _, conditionType := range []string{"POST_MOOD_COUNT", "INTERACTIVE_GAME_COUNT", "CP_BIND_COUNT"} { t.Run(conditionType, func(t *testing.T) { _, err := service.SaveConfig(context.Background(), SaveConfigRequest{ SysOrigin: "LIKEI", Enabled: true, Timezone: "Asia/Riyadh", TaskCategory: TaskCategoryDaily, Tasks: []SaveTaskConfigInput{ { TaskCode: "REMOVED_" + conditionType, Enabled: true, TaskName: "Removed condition", ConditionType: conditionType, TargetValue: 1, TargetUnit: "count", RewardGold: 100, }, }, }) if err == nil { t.Fatalf("SaveConfig() error = nil, want invalid condition") } }) } } type taskCenterTestGateway struct { changedEvents []string changedCommands []integration.GoldReceiptCommand events map[string]bool } func (g *taskCenterTestGateway) ExistsGoldEvent(_ context.Context, eventID string) (bool, error) { return g.events[eventID], nil } func (g *taskCenterTestGateway) ChangeGoldBalance(_ context.Context, cmd integration.GoldReceiptCommand) error { g.changedEvents = append(g.changedEvents, cmd.EventID) g.changedCommands = append(g.changedCommands, cmd) g.events[cmd.EventID] = true return nil }