2026-06-27 16:59:30 +08:00

251 lines
9.9 KiB
Go

package service
import (
"slices"
"testing"
"time"
"google.golang.org/protobuf/proto"
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
roomv1 "hyapp.local/api/proto/room/v1"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/services/room-service/internal/room/command"
"hyapp/services/room-service/internal/room/state"
)
func TestBuildRoomGiftSentRecordsPreservesDurableFactFields(t *testing.T) {
now := time.Date(2026, 6, 27, 10, 0, 0, 0, time.UTC)
cmd := command.SendGift{
Base: command.Base{
CommandID: "cmd-gift-fact",
ActorID: 101,
Room: "room-gift-fact",
},
TargetType: "user",
TargetUserID: 202,
TargetUserIDs: []int64{202},
GiftID: "gift-rose",
PoolID: "pool-lucky",
GiftCount: 3,
SenderCountryID: 86,
SenderRegionID: 9001,
SenderDisplayProfile: command.GiftDisplayProfile{
UserID: 101,
Username: " sender ",
Avatar: " avatar-sender ",
DisplayUserID: "S101",
PrettyDisplayUserID: "PS101",
},
TargetDisplayProfiles: []command.GiftDisplayProfile{{
UserID: 202,
Username: " receiver ",
Avatar: " avatar-receiver ",
DisplayUserID: "R202",
PrettyDisplayUserID: "PR202",
}},
RobotWalletGift: true,
SyntheticLuckyGift: true,
}
billing := &walletv1.DebitGiftResponse{
BillingReceiptId: "receipt-target-202",
CoinSpent: 300,
HeatValue: 450,
GiftTypeCode: "lucky",
CpRelationType: "couple",
GiftName: "Rose",
GiftIconUrl: "https://asset/icon.png",
GiftAnimationUrl: "https://asset/anim.svga",
GiftEffectTypes: []string{"room_banner", "global_broadcast"},
}
records, err := buildRoomGiftSentRecords("room-gift-fact", 7, now, cmd, RoomMeta{VisibleRegionID: 8008}, []giftTargetBilling{{
TargetUserID: 202,
CommandID: "cmd-gift-fact:202",
Billing: billing,
}}, map[int64]int64{202: 999})
if err != nil {
t.Fatalf("build RoomGiftSent record failed: %v", err)
}
if len(records) != 1 {
t.Fatalf("RoomGiftSent must be split per target, got %d records", len(records))
}
record := records[0]
if record.EventType != "RoomGiftSent" || record.RoomID != "room-gift-fact" || record.Envelope.GetRoomVersion() != 7 {
t.Fatalf("outbox envelope mismatch: %+v", record)
}
var event roomeventsv1.RoomGiftSent
if err := proto.Unmarshal(record.Envelope.GetBody(), &event); err != nil {
t.Fatalf("unmarshal RoomGiftSent failed: %v", err)
}
if event.GetSenderUserId() != 101 || event.GetTargetUserId() != 202 || event.GetGiftId() != "gift-rose" || event.GetGiftCount() != 3 {
t.Fatalf("basic gift fields mismatch: %+v", &event)
}
if event.GetBillingReceiptId() != "receipt-target-202" || event.GetCoinSpent() != 300 || event.GetGiftValue() != 450 || event.GetTargetGiftValue() != 999 {
t.Fatalf("billing fields mismatch: %+v", &event)
}
if event.GetVisibleRegionId() != 8008 || event.GetCountryId() != 86 || event.GetRegionId() != 9001 || event.GetCommandId() != "cmd-gift-fact:202" {
t.Fatalf("routing/stat fields mismatch: %+v", &event)
}
if event.GetGiftTypeCode() != "lucky" || event.GetCpRelationType() != "couple" || event.GetGiftName() != "Rose" {
t.Fatalf("gift config fields mismatch: %+v", &event)
}
if event.GetDisplayMode() != "" {
t.Fatalf("single-target durable fact must keep legacy display mode empty: %+v", &event)
}
if !slices.Equal(event.GetGiftEffectTypes(), []string{"room_banner", "global_broadcast"}) {
t.Fatalf("gift effect types mismatch: %+v", event.GetGiftEffectTypes())
}
if !event.GetIsRobotGift() || !event.GetSyntheticLuckyGift() || !event.GetRealRoomRobotGift() {
t.Fatalf("robot/lucky flags mismatch: %+v", &event)
}
if event.GetSenderName() != "sender" || event.GetReceiverNickname() != "receiver" || event.GetReceiverAvatar() != "avatar-receiver" {
t.Fatalf("display profile fields mismatch: %+v", &event)
}
}
func TestBuildRoomGiftSentRecordsKeepsBatchDisplayFactsSplitPerTarget(t *testing.T) {
now := time.Date(2026, 6, 27, 10, 30, 0, 0, time.UTC)
cmd := command.SendGift{
Base: command.Base{
CommandID: "cmd-gift-batch-facts",
ActorID: 101,
Room: "room-gift-batch-facts",
},
TargetType: "user",
TargetUserID: 202,
TargetUserIDs: []int64{202, 203},
GiftID: "gift-clover",
GiftCount: 1,
DisplayMode: "batch",
}
billing := &walletv1.DebitGiftResponse{
BillingReceiptId: "receipt-target",
CoinSpent: 100,
HeatValue: 100,
}
records, err := buildRoomGiftSentRecords("room-gift-batch-facts", 9, now, cmd, RoomMeta{}, []giftTargetBilling{
{TargetUserID: 202, CommandID: "cmd-gift-batch-facts:202", Billing: billing},
{TargetUserID: 203, CommandID: "cmd-gift-batch-facts:203", Billing: billing},
}, nil)
if err != nil {
t.Fatalf("build batch RoomGiftSent records failed: %v", err)
}
if len(records) != 2 {
t.Fatalf("batch gift facts must stay split per target, got %d records", len(records))
}
for _, record := range records {
var event roomeventsv1.RoomGiftSent
if err := proto.Unmarshal(record.Envelope.GetBody(), &event); err != nil {
t.Fatalf("unmarshal batch RoomGiftSent failed: %v", err)
}
if event.GetTargetUserId() <= 0 || event.GetCommandId() == "" {
t.Fatalf("batch RoomGiftSent fact must keep per-target fields: %+v", &event)
}
if event.GetDisplayMode() != "batch" {
t.Fatalf("batch RoomGiftSent fact must be marked for display suppression: %+v", &event)
}
}
}
func TestRoomGiftBatchSentEventFromDisplayPreservesTargetsAndTotals(t *testing.T) {
cmd := command.SendGift{
Base: command.Base{
CommandID: "cmd-batch-fact",
ActorID: 301,
Room: "room-batch-fact",
},
TargetUserID: 401,
TargetUserIDs: []int64{401, 402},
GiftID: "gift-star",
PoolID: "pool-batch",
GiftCount: 2,
DisplayMode: "batch",
SenderDisplayProfile: command.GiftDisplayProfile{
UserID: 301,
Username: "batch sender",
Avatar: "sender-avatar",
DisplayUserID: "S301",
PrettyDisplayUserID: "PS301",
},
TargetDisplayProfiles: []command.GiftDisplayProfile{
{UserID: 401, Username: "target-one", Avatar: "avatar-one", DisplayUserID: "T401", PrettyDisplayUserID: "PT401"},
{UserID: 402, Username: "target-two", Avatar: "avatar-two", DisplayUserID: "T402", PrettyDisplayUserID: "PT402"},
},
}
billing := &walletv1.DebitGiftResponse{
BillingReceiptId: "receipt-batch-root",
HeatValue: 330,
CoinSpent: 220,
GiftTypeCode: "normal",
CpRelationType: "brother",
GiftName: "Star",
GiftIconUrl: "icon-star",
GiftAnimationUrl: "anim-star",
GiftEffectTypes: []string{"room_banner"},
}
targetBillings := []giftTargetBilling{
{TargetUserID: 401, CommandID: "cmd-batch-fact:401", Billing: &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-401", HeatValue: 110, CoinSpent: 70}},
{TargetUserID: 402, CommandID: "cmd-batch-fact:402", Billing: &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-402", HeatValue: 220, CoinSpent: 150}},
}
luckyGifts := []*roomv1.LuckyGiftDrawResult{{
Enabled: true,
DrawId: "draw-402",
CommandId: "cmd-batch-fact:402",
PoolId: "pool-batch",
GiftId: "gift-star",
MultiplierPpm: 2000000,
EffectiveRewardCoins: 50,
TargetUserId: 402,
}}
display := buildSendGiftBatchDisplay(cmd, billing, targetBillings, map[int64]int64{401: 1001, 402: 1002}, luckyGifts, 8888)
event := roomGiftBatchSentEventFromDisplay(display, 6006)
if event.GetSenderUserId() != 301 || event.GetGiftId() != "gift-star" || event.GetGiftCount() != 2 || event.GetCommandId() != "cmd-batch-fact" {
t.Fatalf("batch root fields mismatch: %+v", event)
}
if event.GetTotalGiftValue() != 330 || event.GetTotalCoinSpent() != 220 || event.GetBillingReceiptId() != "receipt-batch-root" || event.GetRoomHeat() != 8888 {
t.Fatalf("batch total fields mismatch: %+v", event)
}
if event.GetVisibleRegionId() != 6006 || !slices.Equal(event.GetTargetUserIds(), []int64{401, 402}) || !slices.Equal(event.GetGiftEffectTypes(), []string{"room_banner"}) {
t.Fatalf("batch routing/effect fields mismatch: %+v", event)
}
if len(event.GetTargets()) != 2 {
t.Fatalf("batch targets mismatch: %+v", event.GetTargets())
}
if target := event.GetTargets()[0]; target.GetTargetUserId() != 401 || target.GetGiftValue() != 110 || target.GetTargetGiftValue() != 1001 || target.GetBillingReceiptId() != "receipt-401" {
t.Fatalf("first batch target mismatch: %+v", target)
}
if target := event.GetTargets()[1]; target.GetTargetUserId() != 402 || target.GetGiftValue() != 220 || target.GetTargetGiftValue() != 1002 || target.GetLuckyGift().GetDrawId() != "draw-402" {
t.Fatalf("second batch target mismatch: %+v", target)
}
}
func TestRoomHeatAndRankChangedRecordsPreserveSnapshotFields(t *testing.T) {
now := time.Date(2026, 6, 27, 11, 0, 0, 0, time.UTC)
heatRecord, err := buildRoomHeatChangedRecord("room-heat-rank", 12, now, 77, 1007)
if err != nil {
t.Fatalf("build RoomHeatChanged failed: %v", err)
}
var heat roomeventsv1.RoomHeatChanged
if err := proto.Unmarshal(heatRecord.Envelope.GetBody(), &heat); err != nil {
t.Fatalf("unmarshal RoomHeatChanged failed: %v", err)
}
if heatRecord.EventType != "RoomHeatChanged" || heat.GetDelta() != 77 || heat.GetCurrentHeat() != 1007 {
t.Fatalf("heat event mismatch: record=%+v event=%+v", heatRecord, &heat)
}
rankRecord, err := buildRoomRankChangedRecord("room-heat-rank", 13, now, state.RankItem{UserID: 501, Score: 700, GiftValue: 700})
if err != nil {
t.Fatalf("build RoomRankChanged failed: %v", err)
}
var rank roomeventsv1.RoomRankChanged
if err := proto.Unmarshal(rankRecord.Envelope.GetBody(), &rank); err != nil {
t.Fatalf("unmarshal RoomRankChanged failed: %v", err)
}
if rankRecord.EventType != "RoomRankChanged" || rank.GetUserId() != 501 || rank.GetScore() != 700 || rank.GetGiftValue() != 700 {
t.Fatalf("rank event mismatch: record=%+v event=%+v", rankRecord, &rank)
}
}