package router import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "chatapp3-golang/internal/common" "chatapp3-golang/internal/integration" "chatapp3-golang/internal/service/gameprovider" "github.com/gin-gonic/gin" ) func TestPublicRoomGameListResponseKeepsLegacyProviderFields(t *testing.T) { resp := publicRoomGameListResponse(&gameprovider.RoomGameListResponse{ Items: []gameprovider.RoomGameListItem{ { ID: 9527, GameID: "bs_1046", GameType: "BAISHUN", Provider: "BAISHUN", ProviderGameID: "1046", Name: "Lucky77Pro", }, }, }) if len(resp.Items) != 1 { t.Fatalf("items = %d, want 1", len(resp.Items)) } item := resp.Items[0] if item.Provider != "BAISHUN" { t.Fatalf("provider = %q, want BAISHUN", item.Provider) } if item.VendorType != "BAISHUN" { t.Fatalf("vendorType = %q, want BAISHUN", item.VendorType) } if item.ProviderGameID != "1046" { t.Fatalf("providerGameId = %q, want 1046", item.ProviderGameID) } if item.VendorGameID != "1046" { t.Fatalf("vendorGameId = %q, want 1046", item.VendorGameID) } } func TestPublicRoomGameListResponseMapsLingxianToLeaderForOldClients(t *testing.T) { resp := publicRoomGameListResponse(&gameprovider.RoomGameListResponse{ Items: []gameprovider.RoomGameListItem{ { ID: 9528, GameID: "1090", GameType: "LINGXIAN", Provider: "LINGXIAN", ProviderGameID: "1090", Name: "Lingxian Game", LaunchParams: map[string]any{"gameType": "LINGXIAN", "screenMode": "half"}, }, }, }) if len(resp.Items) != 1 { t.Fatalf("items = %d, want 1", len(resp.Items)) } item := resp.Items[0] if item.Provider != "LEADER" { t.Fatalf("provider = %q, want LEADER", item.Provider) } if item.GameType != "LEADER" { t.Fatalf("gameType = %q, want LEADER", item.GameType) } if item.VendorType != "LEADER" { t.Fatalf("vendorType = %q, want LEADER", item.VendorType) } if got := item.LaunchParams["gameType"]; got != "LEADER" { t.Fatalf("launchParams.gameType = %v, want LEADER", got) } if got := item.LaunchParams["screenMode"]; got != "half" { t.Fatalf("launchParams.screenMode = %v, want half", got) } } func TestPublicRoomGameListResponseMapsHotgameAliasesToLeaderForOldClients(t *testing.T) { for _, alias := range []string{"HOTGAME", "HOT_GAME", "REYOU", "LALU"} { resp := publicRoomGameListResponse(&gameprovider.RoomGameListResponse{ Items: []gameprovider.RoomGameListItem{ { ID: 9530, GameID: "hg_1", GameType: alias, Provider: alias, ProviderGameID: "1", Name: "Hotgame", LaunchParams: map[string]any{"gameType": alias, "screenMode": "seven"}, }, }, }) if len(resp.Items) != 1 { t.Fatalf("%s items = %d, want 1", alias, len(resp.Items)) } item := resp.Items[0] if item.Provider != "LEADER" { t.Fatalf("%s provider = %q, want LEADER", alias, item.Provider) } if item.GameType != "LEADER" { t.Fatalf("%s gameType = %q, want LEADER", alias, item.GameType) } if item.VendorType != "LEADER" { t.Fatalf("%s vendorType = %q, want LEADER", alias, item.VendorType) } if got := item.LaunchParams["gameType"]; got != "LEADER" { t.Fatalf("%s launchParams.gameType = %v, want LEADER", alias, got) } } } func TestPublicLaunchKeepsLegacyBridgeAndProviderFields(t *testing.T) { launchConfig := map[string]any{"code": "launch-code"} resp := publicLaunch(&gameprovider.LaunchResponse{ ID: 9527, GameSessionID: "bs_room_1", Provider: "BAISHUN", GameID: "bs_1046", ProviderGameID: "1046", LaunchConfig: launchConfig, RoomState: gameprovider.RoomStateResponse{ RoomID: "room-1", State: "PLAYING", Provider: "BAISHUN", CurrentProviderGameID: "1046", }, }) if resp.Provider != "BAISHUN" { t.Fatalf("provider = %q, want BAISHUN", resp.Provider) } if resp.VendorType != "BAISHUN" { t.Fatalf("vendorType = %q, want BAISHUN", resp.VendorType) } if resp.VendorGameID != "1046" { t.Fatalf("vendorGameId = %q, want 1046", resp.VendorGameID) } bridgeConfig, ok := resp.BridgeConfig.(map[string]any) if !ok { t.Fatalf("bridgeConfig type = %T, want map[string]any", resp.BridgeConfig) } if got := bridgeConfig["code"]; got != "launch-code" { t.Fatalf("bridgeConfig[code] = %v, want launch-code", got) } if resp.RoomState.Provider != "BAISHUN" { t.Fatalf("roomState.provider = %q, want BAISHUN", resp.RoomState.Provider) } if resp.RoomState.CurrentVendorGameID != "1046" { t.Fatalf("roomState.currentVendorGameId = %q, want 1046", resp.RoomState.CurrentVendorGameID) } } func TestPublicLaunchMapsLingxianToLeaderForOldClients(t *testing.T) { resp := publicLaunch(&gameprovider.LaunchResponse{ ID: 9528, GameSessionID: "lx_1_1090", Provider: "LINGXIAN", GameID: "1090", ProviderGameID: "1090", LaunchConfig: map[string]any{"uid": "user-1"}, RoomState: gameprovider.RoomStateResponse{ RoomID: "room-1", State: "PLAYING", Provider: "LINGXIAN", CurrentProviderGameID: "1090", }, }) if resp.Provider != "LEADER" { t.Fatalf("provider = %q, want LEADER", resp.Provider) } if resp.VendorType != "LEADER" { t.Fatalf("vendorType = %q, want LEADER", resp.VendorType) } if resp.RoomState.Provider != "LEADER" { t.Fatalf("roomState.provider = %q, want LEADER", resp.RoomState.Provider) } } func TestGameProviderRoutesReturnRoomListForTurkeyRegionUser(t *testing.T) { gin.SetMode(gin.TestMode) provider := &routeStubProvider{} engine := gin.New() registerGameProviderRoutes( engine, routeStubAuthGateway{}, gameprovider.NewRegistry(provider), gameprovider.NewVisibilityPolicy( routeStubVisibilityGateway{ region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"}, }, ), ) req := httptest.NewRequest(http.MethodGet, "/app/game/room/list?roomId=1", nil) req.Header.Set("Authorization", "Bearer token") rec := httptest.NewRecorder() engine.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } var payload struct { Body struct { Items []publicRoomGameItem `json:"items"` } `json:"body"` } if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("response json error = %v: %s", err, rec.Body.String()) } if len(payload.Body.Items) != 1 { t.Fatalf("items = %d, want 1", len(payload.Body.Items)) } if provider.listRoomCalls != 1 { t.Fatalf("provider list calls = %d, want 1", provider.listRoomCalls) } } func TestGameProviderRoutesReturnEmptyRoomListForIOSClient(t *testing.T) { gin.SetMode(gin.TestMode) provider := &routeStubProvider{} engine := gin.New() registerGameProviderRoutes( engine, routeStubAuthGateway{}, gameprovider.NewRegistry(provider), nil, ) req := httptest.NewRequest(http.MethodGet, "/app/game/room/list?roomId=1", nil) req.Header.Set("Authorization", "Bearer token") req.Header.Set("Req-Client", "ios") rec := httptest.NewRecorder() engine.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } var payload struct { Body struct { Items []publicRoomGameItem `json:"items"` } `json:"body"` } if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("response json error = %v: %s", err, rec.Body.String()) } if len(payload.Body.Items) != 0 { t.Fatalf("items = %d, want 0", len(payload.Body.Items)) } if provider.listRoomCalls != 0 { t.Fatalf("provider list calls = %d, want 0", provider.listRoomCalls) } } func TestGameProviderRoutesReturnEmptyShortcutForIOSClient(t *testing.T) { gin.SetMode(gin.TestMode) provider := &routeStubProvider{} engine := gin.New() registerGameProviderRoutes( engine, routeStubAuthGateway{}, gameprovider.NewRegistry(provider), nil, ) req := httptest.NewRequest(http.MethodGet, "/app/game/room/shortcut?roomId=1", nil) req.Header.Set("Authorization", "Bearer token") req.Header.Set("Req-Client", " iOS ") rec := httptest.NewRecorder() engine.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } var payload struct { Body []publicRoomGameItem `json:"body"` } if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("response json error = %v: %s", err, rec.Body.String()) } if len(payload.Body) != 0 { t.Fatalf("items = %d, want 0", len(payload.Body)) } if provider.shortcutCalls != 0 { t.Fatalf("provider shortcut calls = %d, want 0", provider.shortcutCalls) } } func TestGameProviderRoutesReturnEmptyProvidersForIOSClient(t *testing.T) { gin.SetMode(gin.TestMode) engine := gin.New() registerGameProviderRoutes( engine, routeStubAuthGateway{}, gameprovider.NewRegistry(&routeStubProvider{}), nil, ) req := httptest.NewRequest(http.MethodGet, "/app/game/providers", nil) req.Header.Set("Authorization", "Bearer token") req.Header.Set("Req-Client", "IOS") rec := httptest.NewRecorder() engine.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } var payload struct { Body struct { Items []gameprovider.ProviderSummary `json:"items"` } `json:"body"` } if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("response json error = %v: %s", err, rec.Body.String()) } if len(payload.Body.Items) != 0 { t.Fatalf("providers = %d, want 0", len(payload.Body.Items)) } } func TestGameProviderRoutesReturnProvidersWhenVisibilityPolicyAllows(t *testing.T) { gin.SetMode(gin.TestMode) engine := gin.New() registerGameProviderRoutes( engine, routeStubAuthGateway{}, gameprovider.NewRegistry(&routeStubProvider{}), gameprovider.NewVisibilityPolicy( routeStubVisibilityGateway{ region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"}, }, ), ) req := httptest.NewRequest(http.MethodGet, "/app/game/providers", nil) req.Header.Set("Authorization", "Bearer token") req.Header.Set("X-Real-IP", "88.255.1.1") rec := httptest.NewRecorder() engine.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } var payload struct { Body struct { Items []gameprovider.ProviderSummary `json:"items"` } `json:"body"` } if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("response json error = %v: %s", err, rec.Body.String()) } if len(payload.Body.Items) != 1 { t.Fatalf("providers = %d, want 1", len(payload.Body.Items)) } if payload.Body.Items[0].Key != "TEST" { t.Fatalf("provider key = %q, want TEST", payload.Body.Items[0].Key) } } func TestProviderLaunchFallsBackToRegistryWhenLegacyProviderCannotFindGame(t *testing.T) { gin.SetMode(gin.TestMode) engine := gin.New() registerGameProviderRoutes( engine, routeStubAuthGateway{}, gameprovider.NewRegistry( &routeLaunchStubProvider{key: "LINGXIAN"}, &routeLaunchStubProvider{key: "HOTGAME", matchGameID: "hg_1"}, ), nil, ) req := httptest.NewRequest(http.MethodPost, "/app/game/providers/LEADER/launch", strings.NewReader(`{"roomId":"room-1","gameId":"hg_1"}`)) req.Header.Set("Authorization", "Bearer token") req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() engine.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } var payload struct { Body publicLaunchResponse `json:"body"` } if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("response json error = %v: %s", err, rec.Body.String()) } if payload.Body.GameID != "hg_1" { t.Fatalf("gameId = %q, want hg_1", payload.Body.GameID) } if payload.Body.Provider != "LEADER" { t.Fatalf("provider = %q, want LEADER for old-client compat", payload.Body.Provider) } if payload.Body.GameSessionID != "hg-session" { t.Fatalf("gameSessionId = %q, want hg-session", payload.Body.GameSessionID) } } type routeStubAuthGateway struct{} func (routeStubAuthGateway) AuthenticateToken(context.Context, string) (integration.UserCredential, error) { return integration.UserCredential{UserID: integration.Int64Value(1001), SysOrigin: "LIKEI"}, nil } func (routeStubAuthGateway) AuthenticateConsoleToken(context.Context, string) (integration.ConsoleAccount, error) { return integration.ConsoleAccount{}, nil } type routeStubVisibilityGateway struct { region integration.UserRegion } func (s routeStubVisibilityGateway) GetUserRegion(context.Context, int64, string) (integration.UserRegion, error) { return s.region, nil } type routeStubProvider struct { listRoomCalls int shortcutCalls int } func (p *routeStubProvider) Key() string { return "TEST" } func (p *routeStubProvider) DisplayName() string { return "Test Provider" } func (p *routeStubProvider) SupportsLaunch(context.Context, gameprovider.AuthUser, gameprovider.LaunchRequest) (bool, error) { return false, nil } func (p *routeStubProvider) ListShortcutGames(context.Context, gameprovider.AuthUser, string) ([]gameprovider.RoomGameListItem, error) { p.shortcutCalls++ return []gameprovider.RoomGameListItem{{ID: 1, GameID: "test_1", Provider: "TEST", ProviderGameID: "1", Name: "Test Game"}}, nil } func (p *routeStubProvider) ListRoomGames(context.Context, gameprovider.AuthUser, string, string) (*gameprovider.RoomGameListResponse, error) { p.listRoomCalls++ return &gameprovider.RoomGameListResponse{ Items: []gameprovider.RoomGameListItem{{ID: 1, GameID: "test_1", Provider: "TEST", ProviderGameID: "1", Name: "Test Game"}}, }, nil } func (p *routeStubProvider) GetRoomState(context.Context, gameprovider.AuthUser, string) (*gameprovider.RoomStateResponse, error) { return &gameprovider.RoomStateResponse{}, nil } func (p *routeStubProvider) LaunchGame(context.Context, gameprovider.AuthUser, gameprovider.LaunchRequest, string) (*gameprovider.LaunchResponse, error) { return nil, common.NewAppError(http.StatusBadRequest, "unsupported", "unsupported") } func (p *routeStubProvider) CloseGame(context.Context, gameprovider.AuthUser, gameprovider.CloseRequest) (*gameprovider.RoomStateResponse, error) { return &gameprovider.RoomStateResponse{}, nil } type routeLaunchStubProvider struct { key string matchGameID string } func (p *routeLaunchStubProvider) Key() string { return p.key } func (p *routeLaunchStubProvider) DisplayName() string { return p.key } func (p *routeLaunchStubProvider) SupportsLaunch(_ context.Context, _ gameprovider.AuthUser, req gameprovider.LaunchRequest) (bool, error) { return p.matchGameID != "" && req.GameID == p.matchGameID, nil } func (p *routeLaunchStubProvider) ListShortcutGames(context.Context, gameprovider.AuthUser, string) ([]gameprovider.RoomGameListItem, error) { return nil, nil } func (p *routeLaunchStubProvider) ListRoomGames(context.Context, gameprovider.AuthUser, string, string) (*gameprovider.RoomGameListResponse, error) { return &gameprovider.RoomGameListResponse{}, nil } func (p *routeLaunchStubProvider) GetRoomState(context.Context, gameprovider.AuthUser, string) (*gameprovider.RoomStateResponse, error) { return &gameprovider.RoomStateResponse{}, nil } func (p *routeLaunchStubProvider) LaunchGame(_ context.Context, _ gameprovider.AuthUser, req gameprovider.LaunchRequest, _ string) (*gameprovider.LaunchResponse, error) { if p.matchGameID == "" || req.GameID != p.matchGameID { return nil, common.NewAppError(http.StatusNotFound, "game_not_found", "game not found") } return &gameprovider.LaunchResponse{ GameSessionID: "hg-session", Provider: p.key, GameID: req.GameID, ProviderGameID: "1", Entry: gameprovider.LaunchEntry{LaunchMode: "H5_REMOTE"}, RoomState: gameprovider.RoomStateResponse{ RoomID: req.RoomID, State: "PLAYING", Provider: p.key, GameSessionID: "hg-session", }, }, nil } func (p *routeLaunchStubProvider) CloseGame(context.Context, gameprovider.AuthUser, gameprovider.CloseRequest) (*gameprovider.RoomStateResponse, error) { return &gameprovider.RoomStateResponse{}, nil }