package service_test import ( "context" "testing" "time" 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 followTestWallet struct{} func (followTestWallet) DebitGift(context.Context, *walletv1.DebitGiftRequest) (*walletv1.DebitGiftResponse, error) { return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-follow-test"}, nil } func (followTestWallet) BatchDebitGift(context.Context, *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error) { return &walletv1.BatchDebitGiftResponse{Aggregate: &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-follow-test"}}, nil } func (followTestWallet) DebitRobotGift(context.Context, *walletv1.DebitRobotGiftRequest) (*walletv1.DebitGiftResponse, error) { return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-robot-follow-test"}, nil } func (followTestWallet) GrantResourceGroup(context.Context, *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) { return &walletv1.ResourceGrantResponse{Grant: &walletv1.ResourceGrant{GrantId: "grant-follow-test"}}, nil } func TestRoomFollowAffectsSnapshotAndFollowedFeed(t *testing.T) { ctx := context.Background() repository := mysqltest.NewRepository(t) svc := roomservice.New(roomservice.Config{ NodeID: "node-follow-test", LeaseTTL: 10 * time.Second, RankLimit: 20, SnapshotEveryN: 1, }, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher()) roomID := "room-follow-flow" ownerID := int64(1001) viewerID := int64(2002) if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{ Meta: roomservice.NewRequestMeta(roomID, ownerID), SeatCount: 10, Mode: "voice", VisibleRegionId: 1001, RoomName: "Follow Flow", RoomAvatar: testRoomCoverURL, RoomShortId: "follow-flow", }); err != nil { t.Fatalf("create room failed: %v", err) } if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{ Meta: roomservice.NewRequestMeta(roomID, viewerID), Role: "audience", }); err != nil { t.Fatalf("join room failed: %v", err) } beforeFollow, err := svc.GetRoomSnapshot(ctx, &roomv1.GetRoomSnapshotRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID}, RoomId: roomID, ViewerUserId: viewerID, }) if err != nil { t.Fatalf("snapshot before follow failed: %v", err) } if beforeFollow.GetIsFollowed() { t.Fatalf("new viewer must not be followed before FollowRoom") } followResp, err := svc.FollowRoom(ctx, &roomv1.FollowRoomRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID}, RoomId: roomID, UserId: viewerID, }) if err != nil { t.Fatalf("follow room failed: %v", err) } if !followResp.GetFollowed() || followResp.GetFollowedAtMs() <= 0 { t.Fatalf("follow response mismatch: %+v", followResp) } afterFollow, err := svc.GetRoomSnapshot(ctx, &roomv1.GetRoomSnapshotRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID}, RoomId: roomID, ViewerUserId: viewerID, }) if err != nil { t.Fatalf("snapshot after follow failed: %v", err) } if !afterFollow.GetIsFollowed() { t.Fatalf("snapshot must expose is_followed=true after FollowRoom") } feedResp, err := svc.ListRoomFeeds(ctx, &roomv1.ListRoomFeedsRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default}, ViewerUserId: viewerID, VisibleRegionId: 1001, Tab: "followed", Limit: 20, }) if err != nil { t.Fatalf("followed feed failed: %v", err) } if len(feedResp.GetRooms()) != 1 || feedResp.GetRooms()[0].GetRoomId() != roomID { t.Fatalf("followed feed must return followed room: %+v", feedResp.GetRooms()) } unfollowResp, err := svc.UnfollowRoom(ctx, &roomv1.UnfollowRoomRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID}, RoomId: roomID, UserId: viewerID, }) if err != nil { t.Fatalf("unfollow room failed: %v", err) } if unfollowResp.GetFollowed() { t.Fatalf("unfollow response mismatch: %+v", unfollowResp) } emptyFeedResp, err := svc.ListRoomFeeds(ctx, &roomv1.ListRoomFeedsRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default}, ViewerUserId: viewerID, VisibleRegionId: 1001, Tab: "followed", Limit: 20, }) if err != nil { t.Fatalf("followed feed after unfollow failed: %v", err) } if len(emptyFeedResp.GetRooms()) != 0 { t.Fatalf("followed feed must be empty after UnfollowRoom: %+v", emptyFeedResp.GetRooms()) } }