2026-06-11 13:33:44 +08:00

182 lines
6.3 KiB
Go

package task_test
import (
"context"
"testing"
"time"
"google.golang.org/grpc"
walletv1 "hyapp.local/api/proto/wallet/v1"
taskdomain "hyapp/services/activity-service/internal/domain/task"
taskservice "hyapp/services/activity-service/internal/service/task"
"hyapp/services/activity-service/internal/testutil/mysqltest"
)
// TestTaskEventQueryAndClaimFlow 覆盖任务定义、事件进度、App 查询、领奖和领奖幂等的主闭环。
func TestTaskEventQueryAndClaimFlow(t *testing.T) {
repository := mysqltest.NewRepository(t)
wallet := &fakeWalletClient{}
svc := taskservice.New(repository, wallet)
now := time.Date(2026, 5, 9, 10, 0, 0, 0, time.FixedZone("CST", 8*60*60))
svc.SetClock(func() time.Time { return now })
definition, created, err := svc.UpsertTaskDefinition(context.Background(), taskdomain.DefinitionCommand{
TaskType: taskdomain.TypeDaily,
Category: "gift",
MetricType: "gift_spend_coin",
Title: "send gift",
TargetValue: 100,
TargetUnit: "coin",
RewardCoinAmount: 10,
Status: taskdomain.StatusActive,
SortOrder: 1,
OperatorAdminID: 90001,
})
if err != nil || !created {
t.Fatalf("create task definition failed: created=%v err=%v", created, err)
}
result, err := svc.ConsumeTaskEvent(context.Background(), taskdomain.Event{
EventID: "evt-gift-1",
EventType: "WalletGiftDebited",
SourceService: "wallet",
UserID: 10001,
MetricType: "gift_spend_coin",
Value: 120,
OccurredAtMS: now.UnixMilli(),
})
if err != nil {
t.Fatalf("ConsumeTaskEvent failed: %v", err)
}
if result.Status != taskdomain.EventStatusConsumed || result.MatchedTaskCount != 1 {
t.Fatalf("event result mismatch: %+v", result)
}
duplicate, err := svc.ConsumeTaskEvent(context.Background(), taskdomain.Event{
EventID: "evt-gift-1",
EventType: "WalletGiftDebited",
SourceService: "wallet",
UserID: 10001,
MetricType: "gift_spend_coin",
Value: 120,
OccurredAtMS: now.UnixMilli(),
})
if err != nil || duplicate.Status != taskdomain.EventStatusConsumed {
t.Fatalf("duplicate event should be acknowledged: result=%+v err=%v", duplicate, err)
}
list, err := svc.ListUserTasks(context.Background(), 10001)
if err != nil {
t.Fatalf("ListUserTasks failed: %v", err)
}
daily := list.Sections[0]
if len(daily.Items) != 1 || daily.Items[0].TaskID != definition.TaskID || daily.Items[0].ProgressValue != 120 || daily.Items[0].UserStatus != taskdomain.StatusCompleted || !daily.Items[0].Claimable {
t.Fatalf("daily task item mismatch: %+v", daily.Items)
}
claim, err := svc.ClaimTaskReward(context.Background(), taskdomain.ClaimCommand{
UserID: 10001,
TaskID: definition.TaskID,
TaskType: taskdomain.TypeDaily,
CycleKey: "2026-05-09",
CommandID: "cmd-claim-1",
})
if err != nil {
t.Fatalf("ClaimTaskReward failed: %v", err)
}
if claim.Status != taskdomain.ClaimStatusGranted || claim.WalletTransactionID != "wtx-task-1" || wallet.calls != 1 {
t.Fatalf("claim result mismatch: claim=%+v wallet_calls=%d", claim, wallet.calls)
}
again, err := svc.ClaimTaskReward(context.Background(), taskdomain.ClaimCommand{
UserID: 10001,
TaskID: definition.TaskID,
TaskType: taskdomain.TypeDaily,
CycleKey: "2026-05-09",
CommandID: "cmd-claim-1",
})
if err != nil {
t.Fatalf("ClaimTaskReward retry failed: %v", err)
}
if again.ClaimID != claim.ClaimID || wallet.calls != 1 {
t.Fatalf("claim idempotency mismatch: first=%+v second=%+v calls=%d", claim, again, wallet.calls)
}
}
func TestTaskEventDimensionFilter(t *testing.T) {
repository := mysqltest.NewRepository(t)
svc := taskservice.New(repository, &fakeWalletClient{})
now := time.Date(2026, 5, 9, 10, 0, 0, 0, time.UTC)
svc.SetClock(func() time.Time { return now })
definition, created, err := svc.UpsertTaskDefinition(context.Background(), taskdomain.DefinitionCommand{
TaskType: taskdomain.TypeDaily,
Category: "gift",
MetricType: taskdomain.MetricGiftSpendCoin,
Title: "send specified gift",
DimensionFilterJSON: `{"gift_id":"gift_1001"}`,
TargetValue: 100,
TargetUnit: "coin",
RewardCoinAmount: 10,
Status: taskdomain.StatusActive,
OperatorAdminID: 90001,
})
if err != nil || !created {
t.Fatalf("create filtered task definition failed: created=%v err=%v", created, err)
}
skipped, err := svc.ConsumeTaskEvent(context.Background(), taskdomain.Event{
EventID: "evt-gift-filter-skip",
EventType: "RoomGiftSent",
SourceService: "room-service",
UserID: 10001,
MetricType: taskdomain.MetricGiftSpendCoin,
Value: 100,
OccurredAtMS: now.UnixMilli(),
DimensionsJSON: `{"gift_id":"gift_2002"}`,
})
if err != nil {
t.Fatalf("ConsumeTaskEvent skip failed: %v", err)
}
if skipped.Status != taskdomain.EventStatusSkipped || skipped.MatchedTaskCount != 0 {
t.Fatalf("non-matching dimensions should skip: %+v", skipped)
}
consumed, err := svc.ConsumeTaskEvent(context.Background(), taskdomain.Event{
EventID: "evt-gift-filter-hit",
EventType: "RoomGiftSent",
SourceService: "room-service",
UserID: 10001,
MetricType: taskdomain.MetricGiftSpendCoin,
Value: 100,
OccurredAtMS: now.UnixMilli(),
DimensionsJSON: `{"gift_id":"gift_1001"}`,
})
if err != nil {
t.Fatalf("ConsumeTaskEvent hit failed: %v", err)
}
if consumed.Status != taskdomain.EventStatusConsumed || consumed.MatchedTaskCount != 1 {
t.Fatalf("matching dimensions should consume: %+v", consumed)
}
list, err := svc.ListUserTasks(context.Background(), 10001)
if err != nil {
t.Fatalf("ListUserTasks failed: %v", err)
}
if len(list.Sections[0].Items) != 1 || list.Sections[0].Items[0].TaskID != definition.TaskID || list.Sections[0].Items[0].ProgressValue != 100 {
t.Fatalf("filtered progress mismatch: %+v", list.Sections[0].Items)
}
}
type fakeWalletClient struct {
calls int
}
func (f *fakeWalletClient) CreditTaskReward(context.Context, *walletv1.CreditTaskRewardRequest, ...grpc.CallOption) (*walletv1.CreditTaskRewardResponse, error) {
f.calls++
return &walletv1.CreditTaskRewardResponse{
TransactionId: "wtx-task-1",
Amount: 10,
GrantedAtMs: 1778292000000,
Balance: &walletv1.AssetBalance{AssetType: "COIN", AvailableAmount: 10},
}, nil
}