package service_test import ( "context" "encoding/json" "testing" "time" roomv1 "hyapp.local/api/proto/room/v1" "hyapp/pkg/appcode" "hyapp/pkg/usermq" "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 TestHandleUserRegionChangedUpdatesOwnerRoomRegionIdempotently(t *testing.T) { ctx := appcode.WithContext(context.Background(), appcode.Default) repository := mysqltest.NewRepository(t) svc := roomservice.New(roomservice.Config{ NodeID: "node-user-region-sync-test", LeaseTTL: 10 * time.Second, RankLimit: 20, SnapshotEveryN: 1, }, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher()) roomID := "room-user-region-sync" ownerID := int64(163212) if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{ Meta: roomservice.NewRequestMeta(roomID, ownerID), SeatCount: 10, Mode: "voice", VisibleRegionId: 3, OwnerCountryCode: "AE", RoomName: "Region Sync Room", RoomAvatar: testRoomCoverURL, RoomShortId: "163212", }); err != nil { t.Fatalf("create room failed: %v", err) } payload, err := json.Marshal(map[string]any{ "user_id": ownerID, "old_country": "AE", "new_country": "BD", "old_region_id": 3, "new_region_id": 2, "source": "admin", "request_id": "req-user-region-sync", "changed_at_ms": int64(1_700_000_789_000), }) if err != nil { t.Fatalf("marshal payload failed: %v", err) } message := usermq.UserOutboxMessage{ AppCode: appcode.Default, EventID: "UserRegionChanged:admin:163212:3:2:1700000789000", EventType: "UserRegionChanged", AggregateType: "user", AggregateID: ownerID, PayloadJSON: string(payload), OccurredAtMS: 1_700_000_789_000, } if err := svc.HandleUserOutboxMessage(ctx, message); err != nil { t.Fatalf("HandleUserOutboxMessage failed: %v", err) } if err := svc.HandleUserOutboxMessage(ctx, message); err != nil { t.Fatalf("replayed HandleUserOutboxMessage failed: %v", err) } meta, exists, err := repository.GetRoomMeta(ctx, roomID) if err != nil || !exists { t.Fatalf("room meta missing after sync: exists=%t err=%v", exists, err) } if meta.VisibleRegionID != 2 { t.Fatalf("rooms visible region mismatch: %+v", meta) } newRegionEntries, err := repository.ListRoomListEntries(ctx, roomservice.RoomListQuery{VisibleRegionID: 2, Tab: "new", Limit: 10}) if err != nil { t.Fatalf("list new region entries failed: %v", err) } if len(newRegionEntries) != 1 || newRegionEntries[0].RoomID != roomID || newRegionEntries[0].VisibleRegionID != 2 || newRegionEntries[0].OwnerCountryCode != "BD" { t.Fatalf("new region list projection mismatch: %+v", newRegionEntries) } countryEntries, err := repository.ListRoomListEntries(ctx, roomservice.RoomListQuery{VisibleRegionID: 2, CountryCode: "BD", Tab: "new", Limit: 10}) if err != nil { t.Fatalf("list new country entries failed: %v", err) } if len(countryEntries) != 1 || countryEntries[0].RoomID != roomID || countryEntries[0].OwnerCountryCode != "BD" { t.Fatalf("new country list projection mismatch: %+v", countryEntries) } oldRegionEntries, err := repository.ListRoomListEntries(ctx, roomservice.RoomListQuery{VisibleRegionID: 3, Tab: "new", Limit: 10}) if err != nil { t.Fatalf("list old region entries failed: %v", err) } if len(oldRegionEntries) != 0 { t.Fatalf("old region list must not contain migrated room: %+v", oldRegionEntries) } }