387 lines
14 KiB
Go
387 lines
14 KiB
Go
package task_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"google.golang.org/grpc"
|
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
|
"hyapp/pkg/appcode"
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestHuwaaTaskRewardPolicyDefaultsNewTasksToPointAndClaimsPoint(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.UTC)
|
|
svc.SetClock(func() time.Time { return now })
|
|
ctx := appcode.WithContext(context.Background(), "huwaa")
|
|
|
|
if err := svc.PublishTaskRewardPolicy(ctx, taskdomain.RewardPolicyCommand{
|
|
InstanceCode: "huwaa-point-live",
|
|
TemplateCode: "huwaa_point_policy",
|
|
TemplateVersion: "v2",
|
|
Status: "active",
|
|
RewardAssetType: taskdomain.RewardAssetPoint,
|
|
RuleJSON: `{"tasks":{"reward_asset_type":"POINT"}}`,
|
|
OperatorAdminID: 90001,
|
|
PublishedAtMS: now.UnixMilli(),
|
|
}); err != nil {
|
|
t.Fatalf("PublishTaskRewardPolicy failed: %v", err)
|
|
}
|
|
definition, created, err := svc.UpsertTaskDefinition(ctx, taskdomain.DefinitionCommand{
|
|
TaskType: taskdomain.TypeDaily,
|
|
Category: "gift",
|
|
MetricType: taskdomain.MetricGiftSpendCoin,
|
|
Title: "send gift for point",
|
|
TargetValue: 100,
|
|
TargetUnit: "coin",
|
|
RewardCoinAmount: 12,
|
|
Status: taskdomain.StatusActive,
|
|
OperatorAdminID: 90001,
|
|
})
|
|
if err != nil || !created {
|
|
t.Fatalf("create Huwaa task definition failed: created=%v err=%v", created, err)
|
|
}
|
|
if definition.RewardAssetType != taskdomain.RewardAssetPoint {
|
|
t.Fatalf("policy default reward asset mismatch: %+v", definition)
|
|
}
|
|
|
|
if _, err := svc.ConsumeTaskEvent(ctx, taskdomain.Event{
|
|
EventID: "evt-huwaa-point-task",
|
|
EventType: "RoomGiftSent",
|
|
SourceService: "room-service",
|
|
UserID: 41001,
|
|
MetricType: taskdomain.MetricGiftSpendCoin,
|
|
Value: 100,
|
|
OccurredAtMS: now.UnixMilli(),
|
|
}); err != nil {
|
|
t.Fatalf("ConsumeTaskEvent failed: %v", err)
|
|
}
|
|
claim, err := svc.ClaimTaskReward(ctx, taskdomain.ClaimCommand{
|
|
UserID: 41001,
|
|
TaskID: definition.TaskID,
|
|
TaskType: taskdomain.TypeDaily,
|
|
CycleKey: "2026-05-09",
|
|
CommandID: "cmd-huwaa-point-task",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ClaimTaskReward failed: %v", err)
|
|
}
|
|
if claim.RewardAssetType != taskdomain.RewardAssetPoint {
|
|
t.Fatalf("claim reward asset mismatch: %+v", claim)
|
|
}
|
|
if wallet.last == nil ||
|
|
wallet.last.GetAppCode() != "huwaa" ||
|
|
wallet.last.GetRewardAssetType() != taskdomain.RewardAssetPoint ||
|
|
wallet.last.GetAmount() != 12 {
|
|
t.Fatalf("wallet POINT reward request mismatch: %+v", wallet.last)
|
|
}
|
|
}
|
|
|
|
func TestPointTaskRewardPolicyIsRejectedOutsideHuwaa(t *testing.T) {
|
|
repository := mysqltest.NewRepository(t)
|
|
svc := taskservice.New(repository, &fakeWalletClient{})
|
|
ctx := appcode.WithContext(context.Background(), "lalu")
|
|
|
|
if err := svc.PublishTaskRewardPolicy(ctx, taskdomain.RewardPolicyCommand{
|
|
InstanceCode: "lalu-point-denied",
|
|
TemplateCode: "huwaa_point_policy",
|
|
TemplateVersion: "v2",
|
|
Status: "active",
|
|
RewardAssetType: taskdomain.RewardAssetPoint,
|
|
RuleJSON: `{}`,
|
|
OperatorAdminID: 90001,
|
|
}); err == nil {
|
|
t.Fatal("Lalu POINT policy publish must be rejected")
|
|
}
|
|
if _, _, err := svc.UpsertTaskDefinition(ctx, taskdomain.DefinitionCommand{
|
|
TaskType: taskdomain.TypeDaily,
|
|
Category: "gift",
|
|
MetricType: taskdomain.MetricGiftSpendCoin,
|
|
Title: "bad point task",
|
|
TargetValue: 100,
|
|
TargetUnit: "coin",
|
|
RewardCoinAmount: 12,
|
|
RewardAssetType: taskdomain.RewardAssetPoint,
|
|
Status: taskdomain.StatusActive,
|
|
OperatorAdminID: 90001,
|
|
}); err == nil {
|
|
t.Fatal("Lalu explicit POINT task definition must be rejected")
|
|
}
|
|
}
|
|
|
|
func TestTaskRewardPointPolicyAppGateWithoutMySQL(t *testing.T) {
|
|
repository := &fakeTaskPolicyRepository{activeAsset: taskdomain.RewardAssetPoint, activeFound: true}
|
|
svc := taskservice.New(repository, &fakeWalletClient{})
|
|
|
|
if err := svc.PublishTaskRewardPolicy(appcode.WithContext(context.Background(), "lalu"), taskdomain.RewardPolicyCommand{
|
|
InstanceCode: "lalu-point-denied",
|
|
TemplateCode: "huwaa_point_policy",
|
|
TemplateVersion: "v2",
|
|
Status: "active",
|
|
RewardAssetType: taskdomain.RewardAssetPoint,
|
|
RuleJSON: `{}`,
|
|
OperatorAdminID: 90001,
|
|
}); err == nil {
|
|
t.Fatal("Lalu POINT task reward policy must be rejected before repository call")
|
|
}
|
|
if repository.publishCalls != 0 {
|
|
t.Fatalf("rejected Lalu POINT policy must not call repository, got %d calls", repository.publishCalls)
|
|
}
|
|
|
|
if err := svc.PublishTaskRewardPolicy(appcode.WithContext(context.Background(), "huwaa"), taskdomain.RewardPolicyCommand{
|
|
InstanceCode: "huwaa-point-live",
|
|
TemplateCode: "huwaa_point_policy",
|
|
TemplateVersion: "v2",
|
|
Status: "active",
|
|
RewardAssetType: taskdomain.RewardAssetPoint,
|
|
RuleJSON: `{}`,
|
|
OperatorAdminID: 90001,
|
|
}); err != nil {
|
|
t.Fatalf("Huwaa POINT task reward policy should reach repository: %v", err)
|
|
}
|
|
if repository.publishCalls != 1 || repository.lastPublishApp != "huwaa" || repository.lastPublish.RewardAssetType != taskdomain.RewardAssetPoint {
|
|
t.Fatalf("Huwaa POINT policy repository payload mismatch: repo=%+v", repository)
|
|
}
|
|
|
|
definition, created, err := svc.UpsertTaskDefinition(appcode.WithContext(context.Background(), "huwaa"), taskdomain.DefinitionCommand{
|
|
TaskType: taskdomain.TypeDaily,
|
|
Category: "gift",
|
|
MetricType: taskdomain.MetricGiftSpendCoin,
|
|
Title: "fake point task",
|
|
TargetValue: 100,
|
|
TargetUnit: "coin",
|
|
RewardCoinAmount: 12,
|
|
Status: taskdomain.StatusActive,
|
|
OperatorAdminID: 90001,
|
|
})
|
|
if err != nil || !created {
|
|
t.Fatalf("Huwaa policy default task definition failed: created=%v err=%v", created, err)
|
|
}
|
|
if definition.RewardAssetType != taskdomain.RewardAssetPoint ||
|
|
repository.activeLookupApp != "huwaa" ||
|
|
repository.lastUpsert.RewardAssetType != taskdomain.RewardAssetPoint {
|
|
t.Fatalf("Huwaa policy default task definition mismatch: definition=%+v repo=%+v", definition, repository)
|
|
}
|
|
}
|
|
|
|
type fakeTaskPolicyRepository struct {
|
|
taskservice.Repository
|
|
|
|
activeAsset string
|
|
activeFound bool
|
|
activeLookupApp string
|
|
publishCalls int
|
|
lastPublish taskdomain.RewardPolicyCommand
|
|
lastPublishApp string
|
|
lastUpsert taskdomain.DefinitionCommand
|
|
}
|
|
|
|
func (f *fakeTaskPolicyRepository) GetActiveTaskRewardAssetType(ctx context.Context, _ int64) (string, bool, error) {
|
|
f.activeLookupApp = appcode.FromContext(ctx)
|
|
return f.activeAsset, f.activeFound, nil
|
|
}
|
|
|
|
func (f *fakeTaskPolicyRepository) PublishTaskRewardPolicy(ctx context.Context, command taskdomain.RewardPolicyCommand, _ int64) error {
|
|
f.publishCalls++
|
|
f.lastPublish = command
|
|
f.lastPublishApp = appcode.FromContext(ctx)
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeTaskPolicyRepository) UpsertTaskDefinition(_ context.Context, command taskdomain.DefinitionCommand, _ int64) (taskdomain.Definition, bool, error) {
|
|
f.lastUpsert = command
|
|
return taskdomain.Definition{
|
|
AppCode: "huwaa",
|
|
TaskID: "fake-point-task",
|
|
TaskType: command.TaskType,
|
|
Category: command.Category,
|
|
MetricType: command.MetricType,
|
|
Title: command.Title,
|
|
TargetValue: command.TargetValue,
|
|
TargetUnit: command.TargetUnit,
|
|
RewardCoinAmount: command.RewardCoinAmount,
|
|
RewardAssetType: command.RewardAssetType,
|
|
Status: command.Status,
|
|
}, true, nil
|
|
}
|
|
|
|
type fakeWalletClient struct {
|
|
calls int
|
|
last *walletv1.CreditTaskRewardRequest
|
|
}
|
|
|
|
func (f *fakeWalletClient) CreditTaskReward(_ context.Context, req *walletv1.CreditTaskRewardRequest, _ ...grpc.CallOption) (*walletv1.CreditTaskRewardResponse, error) {
|
|
f.calls++
|
|
f.last = req
|
|
assetType := req.GetRewardAssetType()
|
|
if assetType == "" {
|
|
assetType = taskdomain.RewardAssetCoin
|
|
}
|
|
return &walletv1.CreditTaskRewardResponse{
|
|
TransactionId: "wtx-task-1",
|
|
Amount: req.GetAmount(),
|
|
GrantedAtMs: 1778292000000,
|
|
Balance: &walletv1.AssetBalance{AssetType: assetType, AvailableAmount: req.GetAmount()},
|
|
}, nil
|
|
}
|