454 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 TestAllRegionRoomListKeepsPinsThenCountryPresenceBuckets(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
svc := roomservice.New(roomservice.Config{
NodeID: "node-all-region-order-test",
LeaseTTL: 10 * time.Second,
RankLimit: 20,
SnapshotEveryN: 1,
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
createPinnedListRoom(t, ctx, svc, "room-global-pin", "global-pin", 3221, 9002, "AE")
createPinnedListRoom(t, ctx, svc, "room-region-pin-all", "region-pin-all", 3222, 9002, "AE")
createPinnedListRoom(t, ctx, svc, "room-country-pin-all", "country-pin-all", 3223, 9001, "US")
createPinnedListRoom(t, ctx, svc, "room-viewer-online-all", "viewer-online-all", 3224, 9001, "US")
createPinnedListRoom(t, ctx, svc, "room-other-online-all", "other-online-all", 3225, 9002, "AE")
createPinnedListRoom(t, ctx, svc, "room-viewer-empty-all", "viewer-empty-all", 3226, 9002, "US")
createPinnedListRoom(t, ctx, svc, "room-other-empty-all", "other-empty-all", 3227, 9001, "AE")
for index, roomID := range []string{
"room-global-pin",
"room-region-pin-all",
"room-country-pin-all",
"room-viewer-online-all",
"room-other-online-all",
"room-viewer-empty-all",
"room-other-empty-all",
} {
repository.SetRoomSortScore(roomID, int64(1000-index))
}
repository.SetRoomPresence("room-viewer-online-all", 1, 0)
repository.SetRoomPresence("room-other-online-all", 1, 0)
repository.SetRoomPresence("room-viewer-empty-all", 0, 0)
repository.SetRoomPresence("room-other-empty-all", 0, 0)
expiresAtMS := time.Now().UTC().Add(24 * time.Hour).UnixMilli()
repository.PinRoomWithType("room-global-pin", "global", 0, 100, expiresAtMS)
repository.PinRoomWithType("room-region-pin-all", "region", 9002, 90, expiresAtMS)
repository.PinRoomWithType("room-country-pin-all", "country", 9001, 80, expiresAtMS)
page, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
ViewerUserId: 4001,
VisibleRegionId: 9001,
ViewerCountryCode: "US",
AllVisibleRegions: true,
Tab: "hot",
Limit: 10,
})
if err != nil {
t.Fatalf("list all-region rooms failed: %v", err)
}
want := []string{
"room-global-pin",
"room-region-pin-all",
"room-country-pin-all",
"room-viewer-online-all",
"room-other-online-all",
"room-viewer-empty-all",
"room-other-empty-all",
}
if got := roomIDs(page.GetRooms()); len(got) != len(want) {
t.Fatalf("all-region room count mismatch: got=%v want=%v", got, want)
} else {
for index := range want {
if got[index] != want[index] {
t.Fatalf("all-region order mismatch: got=%v want=%v", got, want)
}
}
}
}
func TestAllRegionPinTypePrecedenceMatchesRedisFallbackOrder(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
svc := roomservice.New(roomservice.Config{
NodeID: "node-pin-type-precedence-test",
LeaseTTL: 10 * time.Second,
RankLimit: 20,
SnapshotEveryN: 1,
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
createPinnedListRoom(t, ctx, svc, "room-pin-a", "pin-a", 3241, 9001, "US")
createPinnedListRoom(t, ctx, svc, "room-pin-b", "pin-b", 3242, 9001, "US")
expiresAtMS := time.Now().UTC().Add(24 * time.Hour).UnixMilli()
// MySQL CASE 对同房间固定先选 global=10不会让 region=100 覆盖;因此 B(region=50) 必须排在 A 前。
repository.PinRoomWithType("room-pin-a", "global", 0, 10, expiresAtMS)
repository.PinRoomWithType("room-pin-a", "region", 9001, 100, expiresAtMS)
repository.PinRoomWithType("room-pin-b", "region", 9001, 50, expiresAtMS)
page, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
ViewerUserId: 4001,
VisibleRegionId: 9001,
AllVisibleRegions: true,
Tab: "hot",
Limit: 10,
})
if err != nil {
t.Fatalf("list fallback pin precedence failed: %v", err)
}
if got := roomIDs(page.GetRooms()); len(got) != 2 || got[0] != "room-pin-b" || got[1] != "room-pin-a" {
t.Fatalf("MySQL fallback must order B(region=50) before A(global=10 suppressing region=100), got=%v", got)
}
}
func TestRoomListRegionFilterBindsCursorAndCountryFilterSpansRegions(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
svc := roomservice.New(roomservice.Config{
NodeID: "node-room-filter-test",
LeaseTTL: 10 * time.Second,
RankLimit: 20,
SnapshotEveryN: 1,
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
createPinnedListRoom(t, ctx, svc, "room-region-2-newer", "region-2-newer", 3231, 9002, "AE")
createPinnedListRoom(t, ctx, svc, "room-region-2-older", "region-2-older", 3232, 9002, "AE")
createPinnedListRoom(t, ctx, svc, "room-region-3-ae", "region-3-ae", 3233, 9003, "AE")
repository.SetRoomSortScore("room-region-2-newer", 300)
repository.SetRoomSortScore("room-region-2-older", 200)
repository.SetRoomSortScore("room-region-3-ae", 100)
firstPage, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
ViewerUserId: 4001,
VisibleRegionId: 9001,
FilterRegionId: 9002,
Tab: "hot",
Limit: 1,
})
if err != nil {
t.Fatalf("list region-filtered first page failed: %v", err)
}
if got := roomIDs(firstPage.GetRooms()); len(got) != 1 || got[0] != "room-region-2-newer" || firstPage.GetNextCursor() == "" {
t.Fatalf("region-filtered first page mismatch: rooms=%v cursor=%q", got, firstPage.GetNextCursor())
}
if _, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
ViewerUserId: 4001,
VisibleRegionId: 9001,
FilterRegionId: 9003,
Tab: "hot",
Limit: 1,
Cursor: firstPage.GetNextCursor(),
}); err == nil {
t.Fatal("region cursor must not be reusable for another explicit region")
}
countryPage, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
ViewerUserId: 4001,
VisibleRegionId: 9001,
AllVisibleRegions: true,
CountryCode: "AE",
Tab: "hot",
Limit: 10,
})
if err != nil {
t.Fatalf("list all-region country rooms failed: %v", err)
}
if got := roomIDs(countryPage.GetRooms()); len(got) != 3 {
t.Fatalf("country filter must span all mapped room regions, got=%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
}