package service_test import ( "context" "encoding/json" "testing" "time" roomv1 "hyapp.local/api/proto/room/v1" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/appcode" roomservice "hyapp/services/room-service/internal/room/service" "hyapp/services/room-service/internal/testutil/mysqltest" ) func TestListRoomOnlineUsersReturnsOwnerAdminRoleAndGiftValue(t *testing.T) { ctx := context.Background() repository := mysqltest.NewRepository(t) wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{{ BillingReceiptId: "receipt-online-users", CoinSpent: 321, GiftPointAdded: 321, HeatValue: 321, GiftTypeCode: "normal", }}} now := &fixedRoomRocketClock{now: time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC)} svc := newRocketTestService(t, repository, wallet, now) roomID := "room-online-users" ownerID := int64(1001) adminID := int64(1002) normalID := int64(1003) createRocketRoom(t, ctx, svc, roomID, ownerID, 9001) joinRocketRoom(t, ctx, svc, roomID, adminID) joinRocketRoom(t, ctx, svc, roomID, normalID) if _, err := svc.SetRoomAdmin(ctx, &roomv1.SetRoomAdminRequest{ Meta: roomservice.NewRequestMeta(roomID, ownerID), TargetUserId: adminID, Enabled: true, }); err != nil { t.Fatalf("set admin failed: %v", err) } if _, err := svc.SetRoomAdmin(ctx, &roomv1.SetRoomAdminRequest{ Meta: roomservice.NewRequestMeta(roomID, ownerID), TargetUserId: ownerID, Enabled: true, }); err == nil { t.Fatalf("setting owner as admin must fail") } if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{ Meta: rocketMeta(roomID, adminID, "online-users-gift"), TargetType: "user", TargetUserId: ownerID, GiftId: "gift-online-users", GiftCount: 1, }); err != nil { t.Fatalf("send gift failed: %v", err) } resp, err := svc.ListRoomOnlineUsers(ctx, &roomv1.ListRoomOnlineUsersRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID}, RoomId: roomID, ViewerUserId: ownerID, Page: 1, PageSize: 10, Sort: "gift_value", }) if err != nil { t.Fatalf("list online users failed: %v", err) } if len(resp.GetItems()) != 3 { t.Fatalf("expected three online users, got %+v", resp.GetItems()) } byID := make(map[int64]*roomv1.RoomOnlineUser, len(resp.GetItems())) for _, item := range resp.GetItems() { byID[item.GetUserId()] = item } if owner := byID[ownerID]; owner == nil || !owner.GetIsOwner() || owner.GetRoomRole() != "normal" { t.Fatalf("owner must be is_owner=true and room_role=normal: %+v", owner) } if admin := byID[adminID]; admin == nil || admin.GetIsOwner() || admin.GetRoomRole() != "admin" || admin.GetGiftValue() != 321 { t.Fatalf("admin gift value or role mismatch: %+v", admin) } if normal := byID[normalID]; normal == nil || normal.GetIsOwner() || normal.GetRoomRole() != "normal" || normal.GetGiftValue() != 0 { t.Fatalf("normal user mismatch: %+v", normal) } payload, err := json.Marshal(resp.GetItems()) if err != nil { t.Fatalf("marshal online users failed: %v", err) } t.Logf("online users response items: %s", payload) } func TestJoinLeaveRoomUpdatesPresenceIncrementally(t *testing.T) { ctx := context.Background() repository := mysqltest.NewRepository(t) now := &fixedRoomRocketClock{now: time.Date(2026, 5, 27, 13, 0, 0, 0, time.UTC)} svc := newRocketTestService(t, repository, &rocketTestWallet{}, now) roomID := "room-presence-incremental" ownerID := int64(1101) viewerID := int64(1102) createRocketRoom(t, ctx, svc, roomID, ownerID, 9001) joinRocketRoom(t, ctx, svc, roomID, viewerID) onlineResp, err := svc.ListRoomOnlineUsers(ctx, &roomv1.ListRoomOnlineUsersRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID}, RoomId: roomID, ViewerUserId: ownerID, Page: 1, PageSize: 10, }) if err != nil { t.Fatalf("list online users after join failed: %v", err) } if item := onlineUserItemByID(onlineResp.GetItems(), viewerID); item == nil || item.GetRole() != "audience" { t.Fatalf("join must upsert viewer active presence: %+v", onlineResp.GetItems()) } if _, err := svc.LeaveRoom(ctx, &roomv1.LeaveRoomRequest{Meta: rocketMeta(roomID, viewerID, "presence-incremental-leave")}); err != nil { t.Fatalf("leave room failed: %v", err) } onlineResp, err = svc.ListRoomOnlineUsers(ctx, &roomv1.ListRoomOnlineUsersRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID}, RoomId: roomID, ViewerUserId: ownerID, Page: 1, PageSize: 10, }) if err != nil { t.Fatalf("list online users after leave failed: %v", err) } if item := onlineUserItemByID(onlineResp.GetItems(), viewerID); item != nil { t.Fatalf("leave must mark only viewer presence left: %+v", onlineResp.GetItems()) } } func TestMarkRoomUserPresenceLeftDoesNotClearAnotherRoom(t *testing.T) { ctx := appcode.WithContext(context.Background(), appcode.Default) repository := mysqltest.NewRepository(t) userID := int64(1201) if err := repository.UpsertRoomUserPresence(ctx, roomservice.RoomPresence{ AppCode: appcode.Default, UserID: userID, RoomID: "room-current-active", Role: "audience", RoomVersion: 2, JoinedAtMS: 1000, LastSeenAtMS: 1000, UpdatedAtMS: 1000, }); err != nil { t.Fatalf("upsert current room presence failed: %v", err) } if err := repository.MarkRoomUserPresenceLeft(ctx, roomservice.RoomPresence{ AppCode: appcode.Default, UserID: userID, RoomID: "room-old-left", RoomVersion: 3, UpdatedAtMS: 2000, }); err != nil { t.Fatalf("mark old room presence left failed: %v", err) } presence, exists, err := repository.GetCurrentRoomPresence(ctx, userID) if err != nil { t.Fatalf("get current room presence failed: %v", err) } if !exists || presence.RoomID != "room-current-active" || presence.Status != "active" { t.Fatalf("old room leave projection must not clear newer room presence: exists=%v presence=%+v", exists, presence) } } func onlineUserItemByID(items []*roomv1.RoomOnlineUser, userID int64) *roomv1.RoomOnlineUser { for _, item := range items { if item.GetUserId() == userID { return item } } return nil } func TestSendGiftPassesHostPeriodScopeToWallet(t *testing.T) { ctx := context.Background() repository := mysqltest.NewRepository(t) wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{{ BillingReceiptId: "receipt-host-period", CoinSpent: 14, GiftPointAdded: 6, HeatValue: 22, GiftTypeCode: "normal", HostPeriodDiamondAdded: 14, HostPeriodCycleKey: "2026-05", }}} now := &fixedRoomRocketClock{now: time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC)} svc := newRocketTestService(t, repository, wallet, now) roomID := "room-host-period-scope" senderID := int64(2001) hostID := int64(2002) createRocketRoom(t, ctx, svc, roomID, senderID, 9001) joinRocketRoom(t, ctx, svc, roomID, hostID) if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{ Meta: rocketMeta(roomID, senderID, "host-period-scope"), TargetType: "user", TargetUserId: hostID, GiftId: "gift-host-period", GiftCount: 2, TargetIsHost: true, TargetHostRegionId: 8801, TargetAgencyOwnerUserId: 30001, }); err != nil { t.Fatalf("send host period gift failed: %v", err) } if wallet.lastDebit == nil || !wallet.lastDebit.GetTargetIsHost() || wallet.lastDebit.GetTargetHostRegionId() != 8801 || wallet.lastDebit.GetTargetAgencyOwnerUserId() != 30001 { t.Fatalf("room-service must pass host period scope to wallet: %+v", wallet.lastDebit) } } func TestSendGiftPassesBagSourceToWallet(t *testing.T) { ctx := context.Background() repository := mysqltest.NewRepository(t) wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{ { BillingReceiptId: "receipt-bag-single", CoinSpent: 20, ChargeAmount: 20, ChargeAssetType: "BAG", HeatValue: 20, GiftTypeCode: "normal", }, { BillingReceiptId: "receipt-bag-batch-1", CoinSpent: 10, ChargeAmount: 10, ChargeAssetType: "BAG", HeatValue: 10, GiftTypeCode: "normal", }, { BillingReceiptId: "receipt-bag-batch-2", CoinSpent: 10, ChargeAmount: 10, ChargeAssetType: "BAG", HeatValue: 10, GiftTypeCode: "normal", }, }} now := &fixedRoomRocketClock{now: time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC)} svc := newRocketTestService(t, repository, wallet, now) roomID := "room-bag-source" senderID := int64(2101) targetOneID := int64(2102) targetTwoID := int64(2103) createRocketRoom(t, ctx, svc, roomID, senderID, 9001) joinRocketRoom(t, ctx, svc, roomID, targetOneID) joinRocketRoom(t, ctx, svc, roomID, targetTwoID) if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{ Meta: rocketMeta(roomID, senderID, "bag-single"), TargetType: "user", TargetUserId: targetOneID, GiftId: "gift-bag-single", GiftCount: 2, EntitlementId: "ent-bag-single", Source: "bag", }); err != nil { t.Fatalf("send single bag gift failed: %v", err) } if wallet.lastDebit == nil || wallet.lastDebit.GetEntitlementId() != "ent-bag-single" || wallet.lastDebit.GetChargeSource() != "bag" { t.Fatalf("single bag gift must pass entitlement/source to wallet: %+v", wallet.lastDebit) } if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{ Meta: rocketMeta(roomID, senderID, "bag-batch"), TargetType: "user", TargetUserId: targetOneID, TargetUserIds: []int64{targetOneID, targetTwoID}, GiftId: "gift-bag-batch", GiftCount: 1, EntitlementId: "ent-bag-batch", Source: "bag", }); err != nil { t.Fatalf("send batch bag gift failed: %v", err) } if wallet.lastBatch == nil || wallet.lastBatch.GetEntitlementId() != "ent-bag-batch" || wallet.lastBatch.GetChargeSource() != "bag" { t.Fatalf("batch bag gift must pass entitlement/source to wallet: %+v", wallet.lastBatch) } }