2026-06-17 23:13:08 +08:00

274 lines
10 KiB
Go

package service_test
import (
"context"
"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/integration"
roomservice "hyapp/services/room-service/internal/room/service"
"hyapp/services/room-service/internal/router"
"hyapp/services/room-service/internal/testutil/mysqltest"
)
func TestRoomUserGiftValueFollowsMicChangeAndResetsAfterLeave(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
now := &fixedRoomRocketClock{now: time.Date(2026, 6, 8, 10, 0, 0, 0, time.UTC)}
wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{{
BillingReceiptId: "receipt-target-heat",
CoinSpent: 1000,
ChargeAmount: 1000,
HeatValue: 117,
GiftTypeCode: "normal",
}}}
svc := newUserGiftValueTestService(repository, wallet, now, "node-user-gift-value")
roomID := "room-user-gift-value"
ownerID := int64(61001)
targetID := int64(61002)
createRocketRoom(t, ctx, svc, roomID, ownerID, 9001)
joinRocketRoom(t, ctx, svc, roomID, targetID)
if _, err := svc.MicUp(ctx, &roomv1.MicUpRequest{
Meta: rocketMeta(roomID, targetID, "target-heat-up"),
SeatNo: 2,
}); err != nil {
t.Fatalf("target mic up failed: %v", err)
}
giftResp, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
Meta: rocketMeta(roomID, ownerID, "target-heat-gift"),
TargetType: "user",
TargetUserId: targetID,
GiftId: "gift-target-heat",
GiftCount: 1,
})
if err != nil {
t.Fatalf("send gift failed: %v", err)
}
if user := onlineUserByID(giftResp.GetRoom(), targetID); user == nil || user.GetGiftValue() != 117 {
t.Fatalf("target online user gift_value mismatch: %+v", user)
}
if seat := seatByNo(giftResp.GetRoom(), 2); seat == nil || seat.GetUserId() != targetID || seat.GetGiftValue() != 117 {
t.Fatalf("target seat gift_value mismatch after gift: %+v", seat)
}
giftEvents := roomGiftSentEvents(t, ctx, repository)
if len(giftEvents) != 1 || giftEvents[0].GetTargetGiftValue() != 117 {
t.Fatalf("RoomGiftSent must carry target cumulative gift value: %+v", giftEvents)
}
changeResp, err := svc.ChangeMicSeat(ctx, &roomv1.ChangeMicSeatRequest{
Meta: rocketMeta(roomID, targetID, "target-heat-change"),
TargetUserId: targetID,
SeatNo: 5,
})
if err != nil {
t.Fatalf("change mic failed: %v", err)
}
if oldSeat := seatByNo(changeResp.GetRoom(), 2); oldSeat == nil || oldSeat.GetUserId() != 0 || oldSeat.GetGiftValue() != 0 {
t.Fatalf("old seat must be empty after change: %+v", oldSeat)
}
if newSeat := seatByNo(changeResp.GetRoom(), 5); newSeat == nil || newSeat.GetUserId() != targetID || newSeat.GetGiftValue() != 117 {
t.Fatalf("gift value must follow user to new seat: %+v", newSeat)
}
if changed := firstMicChangedEventByAction(t, ctx, repository, "change"); changed == nil || changed.GetTargetGiftValue() != 117 {
t.Fatalf("RoomMicChanged/change must carry target gift value: %+v", changed)
}
if _, err := svc.LeaveRoom(ctx, &roomv1.LeaveRoomRequest{Meta: rocketMeta(roomID, targetID, "target-heat-leave")}); err != nil {
t.Fatalf("leave room failed: %v", err)
}
joinRocketRoom(t, ctx, svc, roomID, targetID)
reupResp, err := svc.MicUp(ctx, &roomv1.MicUpRequest{
Meta: rocketMeta(roomID, targetID, "target-heat-reup"),
SeatNo: 1,
})
if err != nil {
t.Fatalf("target mic re-up failed: %v", err)
}
if user := onlineUserByID(reupResp.GetRoom(), targetID); user == nil || user.GetGiftValue() != 0 {
t.Fatalf("gift value must reset after leave and rejoin: %+v", user)
}
if seat := seatByNo(reupResp.GetRoom(), 1); seat == nil || seat.GetUserId() != targetID || seat.GetGiftValue() != 0 {
t.Fatalf("rejoined user seat gift_value must be zero: %+v", seat)
}
}
func TestRobotGiftSkipsMainHeatAndRankOutbox(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
now := &fixedRoomRocketClock{now: time.Date(2026, 6, 8, 11, 0, 0, 0, time.UTC)}
wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{{
BillingReceiptId: "receipt-robot-display-only",
CoinSpent: 100,
ChargeAmount: 100,
HeatValue: 100,
ChargeAssetType: "ROBOT_COIN",
GiftTypeCode: "robot",
}}}
svc := newUserGiftValueTestService(repository, wallet, now, "node-robot-gift-outbox-boundary")
roomID := "robot-room-outbox-boundary"
senderID := int64(63001)
targetID := int64(63002)
createRocketRoom(t, ctx, svc, roomID, senderID, 9001)
joinRocketRoom(t, ctx, svc, roomID, targetID)
beforeMain := outboxEventCounts(t, ctx, repository)
beforeRobot := robotOutboxEventCounts(t, ctx, repository)
if _, err := svc.RobotSendGift(ctx, roomservice.RobotSendGiftInput{
Meta: rocketMeta(roomID, senderID, "robot-display-only"),
TargetUserID: targetID,
GiftID: "robot-display-gift",
GiftCount: 1,
RobotUserIDs: []int64{senderID, targetID},
}); err != nil {
t.Fatalf("robot send gift failed: %v", err)
}
afterMain := outboxEventCounts(t, ctx, repository)
if delta := afterMain["RoomGiftSent"] - beforeMain["RoomGiftSent"]; delta != 0 {
t.Fatalf("robot gift must not create main gift outbox, delta=%d counts=%+v", delta, afterMain)
}
if delta := afterMain["RoomHeatChanged"] - beforeMain["RoomHeatChanged"]; delta != 0 {
t.Fatalf("robot gift must not create main heat outbox, delta=%d counts=%+v", delta, afterMain)
}
if delta := afterMain["RoomRankChanged"] - beforeMain["RoomRankChanged"]; delta != 0 {
t.Fatalf("robot gift must not create main rank outbox, delta=%d counts=%+v", delta, afterMain)
}
afterRobot := robotOutboxEventCounts(t, ctx, repository)
if delta := afterRobot["RoomGiftSent"] - beforeRobot["RoomGiftSent"]; delta != 1 {
t.Fatalf("robot gift must keep one robot gift display outbox, delta=%d counts=%+v", delta, afterRobot)
}
if delta := afterRobot["RoomHeatChanged"] - beforeRobot["RoomHeatChanged"]; delta != 1 {
t.Fatalf("robot gift must keep robot heat display outbox, delta=%d counts=%+v", delta, afterRobot)
}
if delta := afterRobot["RoomRankChanged"] - beforeRobot["RoomRankChanged"]; delta != 1 {
t.Fatalf("robot gift must keep robot rank display outbox, delta=%d counts=%+v", delta, afterRobot)
}
}
func TestRoomUserGiftValueRecoversFromCommandLog(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
now := &fixedRoomRocketClock{now: time.Date(2026, 6, 8, 10, 30, 0, 0, time.UTC)}
wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{{
BillingReceiptId: "receipt-replay-target-heat",
CoinSpent: 1500,
ChargeAmount: 1500,
HeatValue: 367,
GiftTypeCode: "lucky",
}}}
svc := newUserGiftValueTestService(repository, wallet, now, "node-user-gift-replay-writer")
roomID := "room-user-gift-replay"
ownerID := int64(62001)
targetID := int64(62002)
createRocketRoom(t, ctx, svc, roomID, ownerID, 9001)
joinRocketRoom(t, ctx, svc, roomID, targetID)
if _, err := svc.MicUp(ctx, &roomv1.MicUpRequest{
Meta: rocketMeta(roomID, targetID, "replay-up"),
SeatNo: 2,
}); err != nil {
t.Fatalf("target mic up failed: %v", err)
}
if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
Meta: rocketMeta(roomID, ownerID, "replay-gift"),
TargetType: "user",
TargetUserId: targetID,
GiftId: "gift-replay-target-heat",
GiftCount: 1,
}); err != nil {
t.Fatalf("send gift failed: %v", err)
}
if _, err := svc.ChangeMicSeat(ctx, &roomv1.ChangeMicSeatRequest{
Meta: rocketMeta(roomID, targetID, "replay-change"),
TargetUserId: targetID,
SeatNo: 4,
}); err != nil {
t.Fatalf("change mic failed: %v", err)
}
recoveredSvc := newUserGiftValueTestService(repository, &rocketTestWallet{}, now, "node-user-gift-replay-reader")
resp, err := recoveredSvc.GetRoomSnapshot(ctx, &roomv1.GetRoomSnapshotRequest{
Meta: rocketMeta(roomID, ownerID, "replay-snapshot"),
RoomId: roomID,
ViewerUserId: ownerID,
})
if err != nil {
t.Fatalf("recover room snapshot failed: %v", err)
}
if user := onlineUserByID(resp.GetRoom(), targetID); user == nil || user.GetGiftValue() != 367 {
t.Fatalf("recovered online user gift_value mismatch: %+v", user)
}
if oldSeat := seatByNo(resp.GetRoom(), 2); oldSeat == nil || oldSeat.GetUserId() != 0 || oldSeat.GetGiftValue() != 0 {
t.Fatalf("recovered old seat must stay empty: %+v", oldSeat)
}
if newSeat := seatByNo(resp.GetRoom(), 4); newSeat == nil || newSeat.GetUserId() != targetID || newSeat.GetGiftValue() != 367 {
t.Fatalf("recovered seat must keep target gift value after change: %+v", newSeat)
}
}
func outboxEventCounts(t *testing.T, ctx context.Context, repository *mysqltest.Repository) map[string]int {
t.Helper()
records, err := repository.ListPendingOutbox(ctx, 200)
if err != nil {
t.Fatalf("list pending outbox failed: %v", err)
}
counts := make(map[string]int, len(records))
for _, record := range records {
counts[record.EventType]++
}
return counts
}
func robotOutboxEventCounts(t *testing.T, ctx context.Context, repository *mysqltest.Repository) map[string]int {
t.Helper()
records, err := repository.ListPendingRobotOutbox(ctx, 200)
if err != nil {
t.Fatalf("list pending robot outbox failed: %v", err)
}
counts := make(map[string]int, len(records))
for _, record := range records {
counts[record.EventType]++
}
return counts
}
func newUserGiftValueTestService(repository *mysqltest.Repository, wallet *rocketTestWallet, clock *fixedRoomRocketClock, nodeID string) *roomservice.Service {
return roomservice.New(roomservice.Config{
NodeID: nodeID,
LeaseTTL: 10 * time.Second,
RankLimit: 20,
SnapshotEveryN: 100,
Clock: clock,
}, router.NewMemoryDirectory(), repository, wallet, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
}
func firstMicChangedEventByAction(t *testing.T, ctx context.Context, repository *mysqltest.Repository, action string) *roomeventsv1.RoomMicChanged {
t.Helper()
records, err := repository.ListPendingOutbox(ctx, 100)
if err != nil {
t.Fatalf("list pending outbox failed: %v", err)
}
for _, record := range records {
if record.EventType != "RoomMicChanged" {
continue
}
var event roomeventsv1.RoomMicChanged
if err := proto.Unmarshal(record.Envelope.GetBody(), &event); err != nil {
t.Fatalf("decode mic changed event failed: %v", err)
}
if event.GetAction() == action {
return &event
}
}
return nil
}