package service_test import ( "context" "testing" "time" roomv1 "hyapp.local/api/proto/room/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" ) func TestRegionalPinOrdersPublicRoomList(t *testing.T) { ctx := context.Background() repository := mysqltest.NewRepository(t) svc := roomservice.New(roomservice.Config{ NodeID: "node-pin-test", LeaseTTL: 10 * time.Second, RankLimit: 20, SnapshotEveryN: 1, }, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher()) createPinnedListRoom(t, ctx, svc, "room-pin-low", "pin-low", 3001, 9001) createPinnedListRoom(t, ctx, svc, "room-hot-high", "hot-high", 3002, 9001) createPinnedListRoom(t, ctx, svc, "room-hot-mid", "hot-mid", 3003, 9001) repository.SetRoomSortScore("room-pin-low", 10) repository.SetRoomSortScore("room-hot-high", 300) repository.SetRoomSortScore("room-hot-mid", 200) repository.PinRoom("room-pin-low", 9001, 99, time.Now().UTC().Add(24*time.Hour).UnixMilli()) firstPage, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default}, ViewerUserId: 4001, VisibleRegionId: 9001, Tab: "hot", Limit: 1, }) if err != nil { t.Fatalf("list pinned first page failed: %v", err) } if len(firstPage.GetRooms()) != 1 || firstPage.GetRooms()[0].GetRoomId() != "room-pin-low" { t.Fatalf("regional pin must be first even with lower hot score: %+v", firstPage.GetRooms()) } if firstPage.GetNextCursor() == "" { t.Fatalf("first page should return cursor") } secondPage, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default}, ViewerUserId: 4001, VisibleRegionId: 9001, Tab: "hot", Limit: 2, Cursor: firstPage.GetNextCursor(), }) if err != nil { t.Fatalf("list pinned second page failed: %v", err) } if got := roomIDs(secondPage.GetRooms()); len(got) != 2 || got[0] != "room-hot-high" || got[1] != "room-hot-mid" { t.Fatalf("second page should continue with regular hot order, got %+v", got) } } func TestPinnedRoomListOrdersRegionCountryThenCountryBucketsForHot(t *testing.T) { ctx := context.Background() repository := mysqltest.NewRepository(t) svc := roomservice.New(roomservice.Config{ NodeID: "node-pin-hot-test", LeaseTTL: 10 * time.Second, RankLimit: 20, SnapshotEveryN: 1, }, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher()) createPinnedListRoom(t, ctx, svc, "room-region-pin", "region-pin", 3101, 9001, "AE") createPinnedListRoom(t, ctx, svc, "room-country-pin", "country-pin", 3102, 9001, "US") createPinnedListRoom(t, ctx, svc, "room-viewer-country-online", "viewer-online", 3103, 9001, "US") createPinnedListRoom(t, ctx, svc, "room-other-country-online", "other-online", 3104, 9001, "AE") createPinnedListRoom(t, ctx, svc, "room-viewer-country-empty", "viewer-empty", 3105, 9001, "US") createPinnedListRoom(t, ctx, svc, "room-other-country-empty", "other-empty", 3106, 9001, "AE") createPinnedListRoom(t, ctx, svc, "room-other-region-online", "other-region", 3107, 9002, "US") repository.SetRoomSortScore("room-region-pin", 1) repository.SetRoomSortScore("room-country-pin", 2) repository.SetRoomSortScore("room-viewer-country-online", 100) repository.SetRoomSortScore("room-other-country-online", 10000) repository.SetRoomSortScore("room-viewer-country-empty", 100000) repository.SetRoomSortScore("room-other-country-empty", 200000) repository.SetRoomSortScore("room-other-region-online", 300000) repository.SetRoomPresence("room-viewer-country-online", 1, 0) repository.SetRoomPresence("room-other-country-online", 1, 0) repository.SetRoomPresence("room-viewer-country-empty", 0, 0) repository.SetRoomPresence("room-other-country-empty", 0, 0) repository.SetRoomPresence("room-other-region-online", 1, 0) expiresAtMS := time.Now().UTC().Add(24 * time.Hour).UnixMilli() repository.PinRoom("room-region-pin", 9001, 99, expiresAtMS) repository.PinRoomWithType("room-country-pin", "country", 9001, 88, expiresAtMS) page, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default}, ViewerUserId: 4001, VisibleRegionId: 9001, ViewerCountryCode: "US", Tab: "hot", Limit: 6, }) if err != nil { t.Fatalf("list hot rooms failed: %v", err) } if got := roomIDs(page.GetRooms()); len(got) != 6 || got[0] != "room-region-pin" || got[1] != "room-country-pin" || got[2] != "room-viewer-country-online" || got[3] != "room-other-country-online" || got[4] != "room-viewer-country-empty" || got[5] != "room-other-country-empty" { t.Fatalf("hot order mismatch: %+v", got) } } func TestPinnedRoomListOrdersRegionCountryThenCountryBucketsForNew(t *testing.T) { ctx := context.Background() repository := mysqltest.NewRepository(t) svc := roomservice.New(roomservice.Config{ NodeID: "node-pin-new-test", LeaseTTL: 10 * time.Second, RankLimit: 20, SnapshotEveryN: 1, }, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher()) createPinnedListRoom(t, ctx, svc, "room-region-new", "region-new", 3201, 9001, "AE") createPinnedListRoom(t, ctx, svc, "room-country-new", "country-new", 3202, 9001, "US") createPinnedListRoom(t, ctx, svc, "room-viewer-newer", "viewer-newer", 3203, 9001, "US") createPinnedListRoom(t, ctx, svc, "room-viewer-older", "viewer-older", 3204, 9001, "US") createPinnedListRoom(t, ctx, svc, "room-other-newest", "other-newest", 3205, 9001, "AE") createPinnedListRoom(t, ctx, svc, "room-other-region-newest", "other-region-newest", 3206, 9002, "US") repository.SetRoomCreatedAt("room-region-new", 20) repository.SetRoomCreatedAt("room-country-new", 10) repository.SetRoomCreatedAt("room-viewer-newer", 300) repository.SetRoomCreatedAt("room-viewer-older", 100) repository.SetRoomCreatedAt("room-other-newest", 1000) repository.SetRoomCreatedAt("room-other-region-newest", 2000) expiresAtMS := time.Now().UTC().Add(24 * time.Hour).UnixMilli() repository.PinRoom("room-region-new", 9001, 99, expiresAtMS) repository.PinRoomWithType("room-country-new", "country", 9001, 88, expiresAtMS) page, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default}, ViewerUserId: 4001, VisibleRegionId: 9001, ViewerCountryCode: "US", Tab: "new", Limit: 5, }) if err != nil { t.Fatalf("list new rooms failed: %v", err) } if got := roomIDs(page.GetRooms()); len(got) != 5 || got[0] != "room-region-new" || got[1] != "room-country-new" || got[2] != "room-viewer-newer" || got[3] != "room-viewer-older" || got[4] != "room-other-newest" { t.Fatalf("new order mismatch: %+v", got) } } func TestCountryPinOnlyAppliesToSelectedCountryTab(t *testing.T) { ctx := context.Background() repository := mysqltest.NewRepository(t) svc := roomservice.New(roomservice.Config{ NodeID: "node-pin-country-tab-test", LeaseTTL: 10 * time.Second, RankLimit: 20, SnapshotEveryN: 1, }, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher()) createPinnedListRoom(t, ctx, svc, "room-us-country-pin", "us-country-pin", 3251, 9001, "US") createPinnedListRoom(t, ctx, svc, "room-us-hot", "us-hot", 3252, 9001, "US") createPinnedListRoom(t, ctx, svc, "room-ae-country-pin", "ae-country-pin", 3253, 9001, "AE") createPinnedListRoom(t, ctx, svc, "room-ae-hot", "ae-hot", 3254, 9001, "AE") repository.SetRoomSortScore("room-us-country-pin", 1) repository.SetRoomSortScore("room-us-hot", 10000) repository.SetRoomSortScore("room-ae-country-pin", 2) repository.SetRoomSortScore("room-ae-hot", 20000) repository.SetRoomPresence("room-us-hot", 1, 0) repository.SetRoomPresence("room-ae-hot", 1, 0) expiresAtMS := time.Now().UTC().Add(24 * time.Hour).UnixMilli() repository.PinRoomWithType("room-us-country-pin", "country", 9001, 99, expiresAtMS) repository.PinRoomWithType("room-ae-country-pin", "country", 9001, 88, expiresAtMS) page, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default}, ViewerUserId: 4001, VisibleRegionId: 9001, ViewerCountryCode: "US", CountryCode: "AE", Tab: "hot", Limit: 4, }) if err != nil { t.Fatalf("list AE country tab failed: %v", err) } if got := roomIDs(page.GetRooms()); len(got) != 2 || got[0] != "room-ae-country-pin" || got[1] != "room-ae-hot" { t.Fatalf("AE country tab must only apply AE country pin, got %+v", got) } } func TestRoomListFiltersByOwnerCountry(t *testing.T) { ctx := context.Background() repository := mysqltest.NewRepository(t) svc := roomservice.New(roomservice.Config{ NodeID: "node-country-filter-test", LeaseTTL: 10 * time.Second, RankLimit: 20, SnapshotEveryN: 1, }, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher()) createPinnedListRoom(t, ctx, svc, "room-us-low", "us-low", 3301, 9001, "US") createPinnedListRoom(t, ctx, svc, "room-ae", "ae", 3302, 9001, "AE") createPinnedListRoom(t, ctx, svc, "room-us-high", "us-high", 3303, 9001, "US") repository.SetRoomSortScore("room-us-low", 100) repository.SetRoomSortScore("room-ae", 200) repository.SetRoomSortScore("room-us-high", 300) firstPage, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default}, ViewerUserId: 4001, VisibleRegionId: 9001, Tab: "hot", Limit: 1, CountryCode: "us", }) if err != nil { t.Fatalf("list country filtered first page failed: %v", err) } if len(firstPage.GetRooms()) != 1 || firstPage.GetRooms()[0].GetRoomId() != "room-us-high" || firstPage.GetRooms()[0].GetCountryCode() != "US" { t.Fatalf("country filter must return only matching owner country in hot order: %+v", firstPage.GetRooms()) } if firstPage.GetNextCursor() == "" { t.Fatalf("first country page should return cursor") } if _, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default}, ViewerUserId: 4001, VisibleRegionId: 9001, Tab: "hot", Limit: 1, CountryCode: "AE", Cursor: firstPage.GetNextCursor(), }); err == nil { t.Fatalf("country cursor must not be reusable across country tabs") } secondPage, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{ Meta: &roomv1.RequestMeta{AppCode: appcode.Default}, ViewerUserId: 4001, VisibleRegionId: 9001, Tab: "hot", Limit: 1, CountryCode: "US", Cursor: firstPage.GetNextCursor(), }) if err != nil { t.Fatalf("list country filtered second page failed: %v", err) } if len(secondPage.GetRooms()) != 1 || secondPage.GetRooms()[0].GetRoomId() != "room-us-low" || secondPage.GetRooms()[0].GetCountryCode() != "US" { t.Fatalf("second country page mismatch: %+v", secondPage.GetRooms()) } } func createPinnedListRoom(t *testing.T, ctx context.Context, svc *roomservice.Service, roomID string, shortID string, ownerID int64, regionID int64, countryCodes ...string) { t.Helper() ownerCountryCode := "" if len(countryCodes) > 0 { ownerCountryCode = countryCodes[0] } if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{ Meta: roomservice.NewRequestMeta(roomID, ownerID), SeatCount: 10, Mode: "voice", VisibleRegionId: regionID, OwnerCountryCode: ownerCountryCode, RoomName: roomID, RoomAvatar: testRoomCoverURL, RoomShortId: shortID, }); err != nil { t.Fatalf("create room %s failed: %v", roomID, err) } } func roomIDs(rooms []*roomv1.RoomListItem) []string { ids := make([]string, 0, len(rooms)) for _, room := range rooms { ids = append(ids, room.GetRoomId()) } return ids }