311 lines
13 KiB
Go
311 lines
13 KiB
Go
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/pkg/xerr"
|
||
"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
|
||
// checkStrategy 让 room 测试显式区分 fixed_v2 滚动兼容与 dynamic_v3 强设备事实。
|
||
checkStrategy string
|
||
}
|
||
|
||
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",
|
||
StrategyVersion: c.checkStrategy,
|
||
}, 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",
|
||
},
|
||
{
|
||
BillingReceiptId: "receipt-lucky-legacy-token",
|
||
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: "sess-must-not-be-device-1",
|
||
DeviceId: "device-auth-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-auth-session-1" ||
|
||
drawMeta.GetAnchorId() != "101" ||
|
||
drawMeta.GetVisibleRegionId() != 9001 {
|
||
t.Fatalf("lucky draw meta mismatch: %+v", drawMeta)
|
||
}
|
||
|
||
// 旧 access JWT 没有 device_id 时,room 必须原样传空以保留 fixed_v2 兼容性;
|
||
// 不能因为需要一个非空风控键就用 session_id 或 command_id 代替设备。
|
||
if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
|
||
Meta: &roomv1.RequestMeta{
|
||
RequestId: "req-lucky-legacy-token", CommandId: "cmd-lucky-legacy-token",
|
||
ActorUserId: ownerID, RoomId: roomID, SessionId: "sess-legacy-not-device",
|
||
AppCode: appcode.Default, SentAtMs: time.Date(2026, 5, 20, 12, 1, 0, 0, time.UTC).UnixMilli(),
|
||
},
|
||
TargetType: "user", TargetUserId: viewerID, GiftId: "rose", GiftCount: 1, PoolId: "super_lucky",
|
||
}); err != nil {
|
||
t.Fatalf("legacy-token fixed-compatible lucky send failed: %v", err)
|
||
}
|
||
legacyDraw := luckyGift.draws[len(luckyGift.draws)-1].GetLuckyGift()
|
||
if legacyDraw.GetDeviceId() != "" {
|
||
t.Fatalf("legacy-token lucky device_id=%q, want empty instead of session/command fallback", legacyDraw.GetDeviceId())
|
||
}
|
||
|
||
luckyGift.checkStrategy = "dynamic_v3"
|
||
debitsBeforeDynamicReject := len(wallet.debitRequests)
|
||
_, err = svc.SendGift(ctx, &roomv1.SendGiftRequest{
|
||
Meta: &roomv1.RequestMeta{
|
||
RequestId: "req-lucky-dynamic-missing-device", CommandId: "cmd-lucky-dynamic-missing-device",
|
||
ActorUserId: ownerID, RoomId: roomID, SessionId: "sess-still-not-a-device",
|
||
AppCode: appcode.Default, SentAtMs: time.Date(2026, 5, 20, 12, 2, 0, 0, time.UTC).UnixMilli(),
|
||
},
|
||
TargetType: "user", TargetUserId: viewerID, GiftId: "rose", GiftCount: 1, PoolId: "super_lucky",
|
||
})
|
||
if !xerr.IsCode(err, xerr.InvalidArgument) {
|
||
t.Fatalf("dynamic_v3 missing signed device_id should fail before debit, got %v", err)
|
||
}
|
||
if len(wallet.debitRequests) != debitsBeforeDynamicReject {
|
||
t.Fatalf("dynamic_v3 missing device reached wallet: before=%d after=%d", debitsBeforeDynamicReject, len(wallet.debitRequests))
|
||
}
|
||
}
|
||
|
||
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: "sess-must-not-be-device-multi",
|
||
DeviceId: "device-auth-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 firstDraw.GetDeviceId() != "device-auth-session-multi" || secondDraw.GetDeviceId() != "device-auth-session-multi" {
|
||
t.Fatalf("multi-target draws must share the signed JWT device scope: first=%q second=%q", firstDraw.GetDeviceId(), secondDraw.GetDeviceId())
|
||
}
|
||
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())
|
||
}
|
||
}
|