package service_test import ( "context" "testing" "time" "google.golang.org/protobuf/proto" luckygiftv1 "hyapp.local/api/proto/luckygift/v1" roomv1 "hyapp.local/api/proto/room/v1" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/appcode" "hyapp/services/room-service/internal/integration" roomservice "hyapp/services/room-service/internal/room/service" "hyapp/services/room-service/internal/router" "hyapp/services/room-service/internal/testutil/mysqltest" ) type luckyGiftTestClient struct { checks []*luckygiftv1.CheckLuckyGiftRequest draws []*luckygiftv1.ExecuteLuckyGiftDrawRequest batchDraws []*luckygiftv1.BatchExecuteLuckyGiftDrawRequest drawResults []*luckygiftv1.LuckyGiftDrawResult } func (c *luckyGiftTestClient) CheckLuckyGift(_ context.Context, req *luckygiftv1.CheckLuckyGiftRequest) (*luckygiftv1.CheckLuckyGiftResponse, error) { c.checks = append(c.checks, req) return &luckygiftv1.CheckLuckyGiftResponse{ Enabled: true, Reason: "enabled", GiftId: req.GetGiftId(), PoolId: req.GetPoolId(), RuleVersion: 12, ExperiencePool: "novice", }, nil } func (c *luckyGiftTestClient) ExecuteLuckyGiftDraw(_ context.Context, req *luckygiftv1.ExecuteLuckyGiftDrawRequest) (*luckygiftv1.ExecuteLuckyGiftDrawResponse, error) { c.draws = append(c.draws, req) meta := req.GetLuckyGift() if len(c.drawResults) > 0 { index := len(c.draws) - 1 if index >= len(c.drawResults) { index = len(c.drawResults) - 1 } result := proto.Clone(c.drawResults[index]).(*luckygiftv1.LuckyGiftDrawResult) if result.CommandId == "" { result.CommandId = meta.GetCommandId() } if result.PoolId == "" { result.PoolId = meta.GetPoolId() } if result.GiftId == "" { result.GiftId = meta.GetGiftId() } return &luckygiftv1.ExecuteLuckyGiftDrawResponse{Result: result}, nil } return &luckygiftv1.ExecuteLuckyGiftDrawResponse{Result: &luckygiftv1.LuckyGiftDrawResult{ DrawId: "lucky_draw_test", CommandId: meta.GetCommandId(), PoolId: meta.GetPoolId(), GiftId: meta.GetGiftId(), RuleVersion: 12, ExperiencePool: "novice", SelectedTierId: "novice_2x", MultiplierPpm: 2_000_000, BaseRewardCoins: 200, EffectiveRewardCoins: 200, RewardStatus: "granted", WalletTransactionId: "wallet_tx_lucky", CoinBalanceAfter: 8800, CreatedAtMs: 1_779_258_000_000, }}, nil } func (c *luckyGiftTestClient) BatchExecuteLuckyGiftDraw(_ context.Context, req *luckygiftv1.BatchExecuteLuckyGiftDrawRequest) (*luckygiftv1.BatchExecuteLuckyGiftDrawResponse, error) { c.batchDraws = append(c.batchDraws, req) resp := &luckygiftv1.BatchExecuteLuckyGiftDrawResponse{Results: make([]*luckygiftv1.LuckyGiftDrawResult, 0, len(req.GetLuckyGifts()))} for index, meta := range req.GetLuckyGifts() { if len(c.drawResults) > 0 { resultIndex := index if resultIndex >= len(c.drawResults) { resultIndex = len(c.drawResults) - 1 } result := proto.Clone(c.drawResults[resultIndex]).(*luckygiftv1.LuckyGiftDrawResult) if result.CommandId == "" { result.CommandId = meta.GetCommandId() } if result.PoolId == "" { result.PoolId = meta.GetPoolId() } if result.GiftId == "" { result.GiftId = meta.GetGiftId() } resp.Results = append(resp.Results, result) continue } resp.Results = append(resp.Results, &luckygiftv1.LuckyGiftDrawResult{ DrawId: "lucky_draw_test", CommandId: meta.GetCommandId(), PoolId: meta.GetPoolId(), GiftId: meta.GetGiftId(), RuleVersion: 12, ExperiencePool: "novice", SelectedTierId: "novice_2x", MultiplierPpm: 2_000_000, BaseRewardCoins: 200, EffectiveRewardCoins: 200, RewardStatus: "pending", CreatedAtMs: 1_779_258_000_000, }) } return resp, nil } func TestSendGiftReturnsLuckyGiftDrawResult(t *testing.T) { ctx := context.Background() repository := mysqltest.NewRepository(t) wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{{ BillingReceiptId: "receipt-lucky", CoinSpent: 100, GiftPointAdded: 100, HeatValue: 100, GiftTypeCode: "super_lucky", }}} luckyGift := &luckyGiftTestClient{} svc := roomservice.New(roomservice.Config{ NodeID: "node-lucky-test", LeaseTTL: 10 * time.Second, RankLimit: 20, SnapshotEveryN: 1, }, router.NewMemoryDirectory(), repository, wallet, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher(), luckyGift) roomID := "room-lucky-gift" ownerID := int64(101) viewerID := int64(202) createRocketRoom(t, ctx, svc, roomID, ownerID, 9001) joinRocketRoom(t, ctx, svc, roomID, viewerID) resp, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{ Meta: &roomv1.RequestMeta{ RequestId: "req-lucky", CommandId: "cmd-lucky", ActorUserId: ownerID, RoomId: roomID, SessionId: "device-session-1", AppCode: appcode.Default, SentAtMs: time.Date(2026, 5, 20, 12, 0, 0, 0, time.UTC).UnixMilli(), }, TargetType: "user", TargetUserId: viewerID, GiftId: "rose", GiftCount: 1, PoolId: "super_lucky", }) if err != nil { t.Fatalf("send lucky gift failed: %v", err) } if resp.GetLuckyGift().GetDrawId() != "lucky_draw_test" || resp.GetLuckyGift().GetMultiplierPpm() != 2_000_000 || resp.GetLuckyGift().GetEffectiveRewardCoins() != 200 || resp.GetLuckyGift().GetRewardStatus() != "granted" || resp.GetLuckyGift().GetWalletTransactionId() != "wallet_tx_lucky" || resp.GetLuckyGift().GetCoinBalanceAfter() != 0 { t.Fatalf("lucky gift result mismatch: %+v", resp.GetLuckyGift()) } if len(resp.GetLuckyGifts()) != 1 || resp.GetLuckyGifts()[0].GetTargetUserId() != viewerID { t.Fatalf("single-target lucky_gifts response mismatch: %+v", resp.GetLuckyGifts()) } if len(luckyGift.checks) != 1 || luckyGift.checks[0].GetPoolId() != "super_lucky" || luckyGift.checks[0].GetGiftId() != "rose" { t.Fatalf("lucky check request mismatch: %+v", luckyGift.checks) } if len(luckyGift.draws) != 1 { t.Fatalf("expected one lucky draw, got %d", len(luckyGift.draws)) } drawMeta := luckyGift.draws[0].GetLuckyGift() if drawMeta.GetCommandId() != "cmd-lucky" || drawMeta.GetCoinSpent() != 100 || drawMeta.GetDeviceId() != "device-session-1" || drawMeta.GetAnchorId() != "101" || drawMeta.GetVisibleRegionId() != 9001 { t.Fatalf("lucky draw meta mismatch: %+v", drawMeta) } } func TestSendGiftReturnsLuckyGiftDrawResultsForMultipleTargets(t *testing.T) { ctx := context.Background() repository := mysqltest.NewRepository(t) wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{ {BillingReceiptId: "receipt-lucky-202", CoinSpent: 100, ChargeAmount: 100, GiftPointAdded: 100, HeatValue: 100, GiftTypeCode: "super_lucky"}, {BillingReceiptId: "receipt-lucky-303", CoinSpent: 200, ChargeAmount: 200, GiftPointAdded: 200, HeatValue: 200, GiftTypeCode: "super_lucky"}, }} luckyGift := &luckyGiftTestClient{drawResults: []*luckygiftv1.LuckyGiftDrawResult{ {DrawId: "draw-first", MultiplierPpm: 2_000_000, BaseRewardCoins: 200, EffectiveRewardCoins: 200, RewardStatus: "pending", StageFeedback: true, CreatedAtMs: 1_779_258_000_001}, {DrawId: "draw-second", MultiplierPpm: 3_000_000, BaseRewardCoins: 300, EffectiveRewardCoins: 300, RewardStatus: "pending", HighMultiplier: true, CreatedAtMs: 1_779_258_000_002}, }} svc := roomservice.New(roomservice.Config{ NodeID: "node-lucky-multi-test", LeaseTTL: 10 * time.Second, RankLimit: 20, SnapshotEveryN: 1, }, router.NewMemoryDirectory(), repository, wallet, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher(), luckyGift) roomID := "room-lucky-gift-multi" ownerID := int64(101) firstTargetID := int64(202) secondTargetID := int64(303) createRocketRoom(t, ctx, svc, roomID, ownerID, 9001) joinRocketRoom(t, ctx, svc, roomID, firstTargetID) joinRocketRoom(t, ctx, svc, roomID, secondTargetID) resp, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{ Meta: &roomv1.RequestMeta{ RequestId: "req-lucky-multi", CommandId: "cmd-lucky-multi", ActorUserId: ownerID, RoomId: roomID, SessionId: "device-session-multi", AppCode: appcode.Default, SentAtMs: time.Date(2026, 5, 20, 12, 0, 0, 0, time.UTC).UnixMilli(), }, TargetType: "user", TargetUserIds: []int64{firstTargetID, secondTargetID}, GiftId: "rose", GiftCount: 1, PoolId: "super_lucky", }) if err != nil { t.Fatalf("send multi-target lucky gift failed: %v", err) } if len(luckyGift.checks) != 1 || luckyGift.checks[0].GetPoolId() != "super_lucky" { t.Fatalf("multi-target lucky gift should check pool once: %+v", luckyGift.checks) } if len(luckyGift.draws) != 0 || len(luckyGift.batchDraws) != 1 { t.Fatalf("multi-target lucky gift should call one batch draw: draws=%d batch=%d", len(luckyGift.draws), len(luckyGift.batchDraws)) } if len(luckyGift.batchDraws[0].GetLuckyGifts()) != 2 { t.Fatalf("multi-target lucky batch should include two targets: %+v", luckyGift.batchDraws[0]) } firstDraw := luckyGift.batchDraws[0].GetLuckyGifts()[0] secondDraw := luckyGift.batchDraws[0].GetLuckyGifts()[1] if firstDraw.GetCommandId() != "cmd-lucky-multi:target:202" || firstDraw.GetTargetUserId() != firstTargetID || firstDraw.GetCoinSpent() != 100 { t.Fatalf("first target lucky draw mismatch: %+v", firstDraw) } if secondDraw.GetCommandId() != "cmd-lucky-multi:target:303" || secondDraw.GetTargetUserId() != secondTargetID || secondDraw.GetCoinSpent() != 200 { t.Fatalf("second target lucky draw mismatch: %+v", secondDraw) } if resp.GetLuckyGift().GetCommandId() != "cmd-lucky-multi" || resp.GetLuckyGift().GetTargetUserId() != 0 || resp.GetLuckyGift().GetSelectedTierId() != "batch" || resp.GetLuckyGift().GetMultiplierPpm() != 5_000_000 || resp.GetLuckyGift().GetEffectiveRewardCoins() != 500 || resp.GetLuckyGift().GetWalletTransactionId() != "" || resp.GetLuckyGift().GetCoinBalanceAfter() != 0 || !resp.GetLuckyGift().GetStageFeedback() || !resp.GetLuckyGift().GetHighMultiplier() || len(resp.GetLuckyGifts()) != 2 { t.Fatalf("multi-target lucky response must include aggregate and list results: %+v", resp) } if resp.GetLuckyGifts()[0].GetCommandId() != "cmd-lucky-multi:target:202" || resp.GetLuckyGifts()[0].GetMultiplierPpm() != 2_000_000 || resp.GetLuckyGifts()[1].GetCommandId() != "cmd-lucky-multi:target:303" || resp.GetLuckyGifts()[1].GetMultiplierPpm() != 3_000_000 || resp.GetLuckyGifts()[1].GetTargetUserId() != secondTargetID { t.Fatalf("multi-target lucky_gifts response mismatch: %+v", resp.GetLuckyGifts()) } }