2026-06-04 10:06:49 +08:00

321 lines
12 KiB
Go

package growth_test
import (
"context"
"fmt"
"testing"
"time"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
activityevents "hyapp.local/api/proto/events/room/v1"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/appcode"
domain "hyapp/services/activity-service/internal/domain/growth"
growthservice "hyapp/services/activity-service/internal/service/growth"
"hyapp/services/activity-service/internal/testutil/mysqltest"
)
func TestGrowthLevelEventRewardFlow(t *testing.T) {
svc, wallet := newGrowthService(t)
ctx := appcode.WithContext(context.Background(), "lalu")
seedGrowthRules(t, ctx, svc, domain.TrackWealth)
result, err := svc.ConsumeLevelEvent(ctx, domain.ValueEvent{
EventID: "level-event-1",
SourceEventID: "room-event-1",
SourceService: "room-service",
SourceEventType: "RoomGiftSent",
UserID: 10001,
Track: domain.TrackWealth,
MetricType: domain.MetricGiftSpendCoin,
ValueDelta: 250,
OccurredAtMS: fixedGrowthNow().UnixMilli(),
DimensionsJSON: `{"room_id":"room-1"}`,
})
if err != nil {
t.Fatalf("ConsumeLevelEvent failed: %v", err)
}
if !result.LevelChanged || result.PreviousLevel != 0 || result.NewLevel != 2 || result.RewardJobCount != 2 {
t.Fatalf("level result mismatch: %+v", result)
}
duplicate, err := svc.ConsumeLevelEvent(ctx, domain.ValueEvent{
EventID: "level-event-1",
SourceEventID: "room-event-1",
SourceService: "room-service",
SourceEventType: "RoomGiftSent",
UserID: 10001,
Track: domain.TrackWealth,
MetricType: domain.MetricGiftSpendCoin,
ValueDelta: 250,
OccurredAtMS: fixedGrowthNow().UnixMilli(),
DimensionsJSON: `{"room_id":"room-1"}`,
})
if err != nil || duplicate.Status != domain.EventStatusDuplicate {
t.Fatalf("duplicate event mismatch: result=%+v err=%v", duplicate, err)
}
overview, err := svc.GetMyLevelOverview(ctx, 10001)
if err != nil {
t.Fatalf("GetMyLevelOverview failed: %v", err)
}
wealth := findTrackOverview(overview.Tracks, domain.TrackWealth)
if wealth.Level != 2 || wealth.TierID == 0 || wealth.TotalValue != 250 || wealth.DisplayAvatarFrameResourceID != 502 || wealth.DisplayBadgeResourceID != 702 || wealth.DisplayBadgeSourceLevel != 2 || wealth.RewardPendingCount != 2 {
t.Fatalf("wealth overview mismatch: %+v", wealth)
}
profiles, err := svc.BatchGetUserLevelDisplayProfiles(ctx, []int64{10001})
if err != nil {
t.Fatalf("BatchGetUserLevelDisplayProfiles failed: %v", err)
}
if len(profiles.Profiles) != 1 || profiles.Profiles[0].Wealth.BadgeResourceID != 702 || profiles.Profiles[0].Wealth.BadgeSourceLevel != 2 {
t.Fatalf("display profile should use highest configured rule badge: %+v", profiles.Profiles)
}
claimed, processed, success, failure, hasMore, err := svc.ProcessLevelRewardBatch(ctx, "run-1", "worker-1", 10, time.Second)
if err != nil {
t.Fatalf("ProcessLevelRewardBatch failed: %v", err)
}
if claimed != 2 || processed != 2 || success != 2 || failure != 0 || hasMore || len(wallet.grants) != 2 {
t.Fatalf("reward batch mismatch: claimed=%d processed=%d success=%d failure=%d has_more=%v grants=%d", claimed, processed, success, failure, hasMore, len(wallet.grants))
}
rewards, total, err := svc.ListLevelRewards(ctx, domain.RewardQuery{UserID: 10001, Status: domain.RewardStatusGranted})
if err != nil {
t.Fatalf("ListLevelRewards failed: %v", err)
}
if total != 2 || len(rewards) != 2 {
t.Fatalf("granted rewards mismatch: total=%d rewards=%+v", total, rewards)
}
}
func TestGrowthRoomGiftEventFeedsWealthAndCharm(t *testing.T) {
svc, _ := newGrowthService(t)
ctx := appcode.WithContext(context.Background(), "lalu")
seedGrowthRules(t, ctx, svc, domain.TrackWealth)
seedGrowthRules(t, ctx, svc, domain.TrackCharm)
body, err := proto.Marshal(&activityevents.RoomGiftSent{
SenderUserId: 20001,
TargetUserId: 20002,
GiftId: "gift-1",
GiftCount: 1,
GiftValue: 150,
BillingReceiptId: "bill-1",
VisibleRegionId: 86,
CommandId: "cmd-gift-1",
})
if err != nil {
t.Fatalf("marshal room gift failed: %v", err)
}
consumed, err := svc.HandleRoomEvent(ctx, &activityevents.EventEnvelope{
EventId: "room-gift-event-1",
RoomId: "room-1",
EventType: "RoomGiftSent",
RoomVersion: 7,
OccurredAtMs: fixedGrowthNow().UnixMilli(),
Body: body,
AppCode: "lalu",
})
if err != nil {
t.Fatalf("HandleRoomEvent failed: %v", err)
}
if consumed != 2 {
t.Fatalf("room gift should create wealth and charm events, consumed=%d", consumed)
}
sender, err := svc.GetLevelTrack(ctx, 20001, domain.TrackWealth)
if err != nil {
t.Fatalf("GetLevelTrack sender failed: %v", err)
}
target, err := svc.GetLevelTrack(ctx, 20002, domain.TrackCharm)
if err != nil {
t.Fatalf("GetLevelTrack target failed: %v", err)
}
if sender.Overview.Level != 1 || sender.Overview.TotalValue != 150 {
t.Fatalf("sender wealth mismatch: %+v", sender.Overview)
}
if target.Overview.Level != 1 || target.Overview.TotalValue != 150 {
t.Fatalf("target charm mismatch: %+v", target.Overview)
}
}
func TestListLevelConfigReturnsAdminRulesAndTiers(t *testing.T) {
svc, _ := newGrowthService(t)
ctx := appcode.WithContext(context.Background(), "lalu")
seedGrowthRules(t, ctx, svc, domain.TrackGame)
config, err := svc.ListLevelConfig(ctx, domain.ConfigQuery{Track: domain.TrackGame})
if err != nil {
t.Fatalf("ListLevelConfig failed: %v", err)
}
if len(config.Tracks) != 1 || config.Tracks[0].Track != domain.TrackGame {
t.Fatalf("track config mismatch: %+v", config.Tracks)
}
if len(config.Rules) != 2 || config.Rules[0].Level != 1 || config.Rules[1].RequiredValue != 200 {
t.Fatalf("rule config mismatch: %+v", config.Rules)
}
if len(config.Tiers) != 2 || config.Tiers[1].DisplayBadgeResourceID != 602 {
t.Fatalf("tier config mismatch: %+v", config.Tiers)
}
if config.ServerTimeMS != fixedGrowthNow().UnixMilli() {
t.Fatalf("server time mismatch: %d", config.ServerTimeMS)
}
}
func TestZeroLevelRuleAndTierAreValid(t *testing.T) {
svc, _ := newGrowthService(t)
ctx := appcode.WithContext(context.Background(), "lalu")
if _, created, err := svc.UpsertLevelRule(ctx, domain.RuleCommand{
Track: domain.TrackWealth,
Level: 0,
RequiredValue: 0,
Name: "wealth 0",
Status: domain.StatusActive,
SortOrder: 0,
DisplayConfigJSON: `{"long_badge_resource_id":700}`,
OperatorAdminID: 90001,
}); err != nil || !created {
t.Fatalf("seed level rule 0 failed: created=%v err=%v", created, err)
}
if _, created, err := svc.UpsertLevelTier(ctx, domain.TierCommand{
Track: domain.TrackWealth,
MinLevel: 0,
MaxLevel: 9,
Name: "wealth 0-9",
Status: domain.StatusActive,
OperatorAdminID: 90001,
}); err != nil || !created {
t.Fatalf("seed tier 0 failed: created=%v err=%v", created, err)
}
detail, err := svc.GetLevelTrack(ctx, 30001, domain.TrackWealth)
if err != nil {
t.Fatalf("GetLevelTrack failed: %v", err)
}
if detail.Overview.Level != 0 || detail.Overview.CurrentLevelRequiredValue != 0 || detail.Overview.DisplayBadgeResourceID != 700 || detail.Overview.DisplayBadgeSourceLevel != 0 {
t.Fatalf("zero level overview mismatch: %+v", detail.Overview)
}
}
func TestIssueRegistrationLevelBadgesGrantsLevelOneBadges(t *testing.T) {
svc, wallet := newGrowthService(t)
ctx := appcode.WithContext(context.Background(), "lalu")
seedGrowthRules(t, ctx, svc, domain.TrackWealth)
seedGrowthRules(t, ctx, svc, domain.TrackGame)
seedGrowthRules(t, ctx, svc, domain.TrackCharm)
grants, err := svc.IssueRegistrationLevelBadges(ctx, 30001, "registration_level_badges:lalu:30001")
if err != nil {
t.Fatalf("IssueRegistrationLevelBadges failed: %v", err)
}
if len(grants.Grants) != 3 || len(wallet.resourceGrants) != 3 {
t.Fatalf("registration level badge grants mismatch: grants=%+v wallet=%d", grants.Grants, len(wallet.resourceGrants))
}
for _, req := range wallet.resourceGrants {
if req.GetTargetUserId() != 30001 || req.GetResourceId() != 701 || req.GetQuantity() != 1 || req.GetGrantSource() != domain.GrantSourceGrowthLevel {
t.Fatalf("wallet grant request mismatch: %+v", req)
}
}
}
func newGrowthService(t *testing.T) (*growthservice.Service, *fakeGrowthWallet) {
t.Helper()
repository := mysqltest.NewRepository(t)
wallet := &fakeGrowthWallet{}
svc := growthservice.New(repository, wallet)
svc.SetClock(fixedGrowthNow)
return svc, wallet
}
func seedGrowthRules(t *testing.T, ctx context.Context, svc *growthservice.Service, track string) {
t.Helper()
if _, created, err := svc.UpsertLevelRule(ctx, domain.RuleCommand{
Track: track,
Level: 1,
RequiredValue: 100,
Name: track + " 1",
Status: domain.StatusActive,
RewardResourceGroupID: 0,
SortOrder: 1,
DisplayConfigJSON: `{"long_badge_resource_id":701}`,
OperatorAdminID: 90001,
}); err != nil || !created {
t.Fatalf("seed level rule 1 failed: created=%v err=%v", created, err)
}
if _, created, err := svc.UpsertLevelRule(ctx, domain.RuleCommand{
Track: track,
Level: 2,
RequiredValue: 200,
Name: track + " 2",
Status: domain.StatusActive,
RewardResourceGroupID: 9002,
SortOrder: 2,
DisplayConfigJSON: `{"long_badge_resource_id":702}`,
OperatorAdminID: 90001,
}); err != nil || !created {
t.Fatalf("seed level rule 2 failed: created=%v err=%v", created, err)
}
if _, created, err := svc.UpsertLevelTier(ctx, domain.TierCommand{
Track: track,
MinLevel: 1,
MaxLevel: 1,
Name: track + " 1-1",
DisplayAvatarFrameResourceID: 501,
DisplayBadgeResourceID: 601,
RewardResourceGroupID: 9001,
Status: domain.StatusActive,
OperatorAdminID: 90001,
}); err != nil || !created {
t.Fatalf("seed tier 1 failed: created=%v err=%v", created, err)
}
if _, created, err := svc.UpsertLevelTier(ctx, domain.TierCommand{
Track: track,
MinLevel: 2,
MaxLevel: 10,
Name: track + " 2-10",
DisplayAvatarFrameResourceID: 502,
DisplayBadgeResourceID: 602,
RewardResourceGroupID: 9003,
Status: domain.StatusActive,
OperatorAdminID: 90001,
}); err != nil || !created {
t.Fatalf("seed tier 2 failed: created=%v err=%v", created, err)
}
}
func fixedGrowthNow() time.Time {
return time.Date(2026, 5, 14, 4, 0, 0, 0, time.UTC)
}
func findTrackOverview(items []domain.TrackOverview, track string) domain.TrackOverview {
for _, item := range items {
if item.Track == track {
return item
}
}
return domain.TrackOverview{}
}
type fakeGrowthWallet struct {
grants []*walletv1.GrantResourceGroupRequest
resourceGrants []*walletv1.GrantResourceRequest
}
func (f *fakeGrowthWallet) GrantResource(_ context.Context, req *walletv1.GrantResourceRequest, _ ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error) {
f.resourceGrants = append(f.resourceGrants, req)
return &walletv1.ResourceGrantResponse{
Grant: &walletv1.ResourceGrant{
GrantId: fmt.Sprintf("resource-grant-%d", len(f.resourceGrants)),
Status: "succeeded",
},
}, nil
}
func (f *fakeGrowthWallet) GrantResourceGroup(_ context.Context, req *walletv1.GrantResourceGroupRequest, _ ...grpc.CallOption) (*walletv1.ResourceGrantResponse, error) {
f.grants = append(f.grants, req)
return &walletv1.ResourceGrantResponse{
Grant: &walletv1.ResourceGrant{
GrantId: fmt.Sprintf("grant-%d", len(f.grants)),
Status: "succeeded",
},
}, nil
}