package gameopen import ( "chatapp3-golang/internal/config" "chatapp3-golang/internal/integration" "context" "testing" ) type stubGateway struct { credential integration.UserCredential token string authToken string profile integration.UserProfile accountMap map[string]integration.UserProfile balance int64 exists bool cmd integration.GoldReceiptCommand } func (s *stubGateway) AuthenticateToken(_ context.Context, token string) (integration.UserCredential, error) { s.authToken = token return s.credential, nil } func (s *stubGateway) CreateIfAbsentUserCredentialToken(context.Context, int64) (string, error) { return s.token, nil } func (s *stubGateway) GetUserProfile(context.Context, int64) (integration.UserProfile, error) { return s.profile, nil } func (s *stubGateway) GetUserProfileByAccount(_ context.Context, _ string, account string) (integration.UserProfile, error) { if profile, ok := s.accountMap[account]; ok { return profile, nil } return integration.UserProfile{}, context.Canceled } func (s *stubGateway) GetUserProfileByAccountOne(_ context.Context, account string) (integration.UserProfile, error) { if profile, ok := s.accountMap[account]; ok { return profile, nil } return integration.UserProfile{}, context.Canceled } func (s *stubGateway) MapGoldBalance(context.Context, []int64) (map[int64]int64, error) { result := map[int64]int64{} if int64(s.credential.UserID) > 0 { result[int64(s.credential.UserID)] = s.balance } if int64(s.profile.ID) > 0 { result[int64(s.profile.ID)] = s.balance } for _, profile := range s.accountMap { if int64(profile.ID) > 0 { result[int64(profile.ID)] = s.balance } } return result, nil } func (s *stubGateway) ExistsGoldEvent(context.Context, string) (bool, error) { return s.exists, nil } func (s *stubGateway) ChangeGoldBalance(_ context.Context, cmd integration.GoldReceiptCommand) error { s.cmd = cmd return nil } func TestHandleQueryUser(t *testing.T) { gateway := &stubGateway{ credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"}, profile: integration.UserProfile{ ID: 1234567, Account: "1234567", UserNickname: "tester", UserAvatar: "https://example.com/avatar.png", }, balance: 88, } service := NewGameOpenService(config.Config{ GameOpen: config.GameOpenConfig{AppKey: "game-open-test-key"}, }, nil, gateway) req := QueryUserRequest{ GameID: "101", UID: "1234567", Token: "token", RoomID: "room-1", } req.Sign = buildQueryUserSign(req, "game-open-test-key") resp := service.HandleQueryUser(context.Background(), req, "{}") if resp.ErrorCode != 0 { t.Fatalf("HandleQueryUser() error = %+v", resp) } data, ok := resp.Data.(map[string]any) if !ok { t.Fatalf("HandleQueryUser() data type = %T", resp.Data) } if got := data["coin"]; got != int64(88) { t.Fatalf("HandleQueryUser() coin = %v, want 88", got) } } func TestHandleQueryUserNormalizesCallbackTokenAfterSignVerify(t *testing.T) { tests := []struct { name string token string want string }{ {name: "missing_padding", token: "token-sign.YWJjZA", want: "token-sign.YWJjZA=="}, {name: "encoded_padding", token: "token-sign.YWJjZA%3D%3D", want: "token-sign.YWJjZA=="}, {name: "encoded_bearer_missing_padding", token: "Bearer%20token-sign.YWJjZA", want: "token-sign.YWJjZA=="}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gateway := &stubGateway{ credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"}, profile: integration.UserProfile{ ID: 1234567, Account: "1234567", }, balance: 88, } service := NewGameOpenService(config.Config{ GameOpen: config.GameOpenConfig{AppKey: "game-open-test-key"}, }, nil, gateway) req := QueryUserRequest{ GameID: "101", UID: "1234567", Token: tt.token, RoomID: "room-1", } req.Sign = buildQueryUserSign(req, "game-open-test-key") resp := service.HandleQueryUser(context.Background(), req, "{}") if resp.ErrorCode != 0 { t.Fatalf("HandleQueryUser() error = %+v", resp) } if gateway.authToken != tt.want { t.Fatalf("AuthenticateToken() token = %q, want %q", gateway.authToken, tt.want) } }) } } func TestHandleUpdateCoin(t *testing.T) { gateway := &stubGateway{ credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"}, profile: integration.UserProfile{ ID: 1234567, Account: "1234567", }, balance: 99, } service := NewGameOpenService(config.Config{ GameOpen: config.GameOpenConfig{AppKey: "game-open-test-key"}, }, nil, gateway) req := UpdateCoinRequest{ OrderID: "order-1", GameID: "101", RoundID: "round-1", UID: "1234567", Coin: 15, Type: 2, RewardType: 2, Token: "token", WinID: "win-1", RoomID: "room-1", } req.Sign = buildUpdateCoinSign(req, "game-open-test-key") resp := service.HandleUpdateCoin(context.Background(), req, "{}") if resp.ErrorCode != 0 { t.Fatalf("HandleUpdateCoin() error = %+v", resp) } if gateway.cmd.EventID != "GAME_OPEN:order-1" { t.Fatalf("HandleUpdateCoin() event id = %q", gateway.cmd.EventID) } if gateway.cmd.ReceiptType != "INCOME" { t.Fatalf("HandleUpdateCoin() receipt type = %q", gateway.cmd.ReceiptType) } } func TestHandleUpdateCoinNormalizesCallbackTokenAfterSignVerify(t *testing.T) { gateway := &stubGateway{ credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"}, profile: integration.UserProfile{ ID: 1234567, Account: "1234567", }, balance: 99, } service := NewGameOpenService(config.Config{ GameOpen: config.GameOpenConfig{AppKey: "game-open-test-key"}, }, nil, gateway) req := UpdateCoinRequest{ OrderID: "order-1", GameID: "101", RoundID: "round-1", UID: "1234567", Coin: 15, Type: 2, RewardType: 2, Token: "Bearer%20token-sign.YWJjZA", WinID: "win-1", RoomID: "room-1", } req.Sign = buildUpdateCoinSign(req, "game-open-test-key") resp := service.HandleUpdateCoin(context.Background(), req, "{}") if resp.ErrorCode != 0 { t.Fatalf("HandleUpdateCoin() error = %+v", resp) } if gateway.authToken != "token-sign.YWJjZA==" { t.Fatalf("AuthenticateToken() token = %q, want token-sign.YWJjZA==", gateway.authToken) } } func TestHandleUpdateCoinDuplicateOrderReturns10003(t *testing.T) { gateway := &stubGateway{ credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"}, profile: integration.UserProfile{ ID: 1234567, Account: "1234567", }, balance: 99, exists: true, } service := NewGameOpenService(config.Config{ GameOpen: config.GameOpenConfig{AppKey: "game-open-test-key"}, }, nil, gateway) req := UpdateCoinRequest{ OrderID: "27822d8ba4a34d0e8db3e049716d427d", GameID: "101", RoundID: "round-1", UID: "1234567", Coin: 15, Type: 2, RewardType: 2, Token: "token", WinID: "win-1", RoomID: "room-1", } req.Sign = buildUpdateCoinSign(req, "game-open-test-key") resp := service.HandleUpdateCoin(context.Background(), req, "{}") if resp.ErrorCode != gameOpenCodeDuplicateOrder { t.Fatalf("HandleUpdateCoin() errorCode = %d, want %d", resp.ErrorCode, gameOpenCodeDuplicateOrder) } if resp.ErrorMsg != "duplicate orderId" { t.Fatalf("HandleUpdateCoin() errorMsg = %q", resp.ErrorMsg) } if gateway.cmd.EventID != "" { t.Fatalf("HandleUpdateCoin() should not change balance for duplicate order, got event id %q", gateway.cmd.EventID) } } func TestHandleQueryUserAcceptsAccountUID(t *testing.T) { gateway := &stubGateway{ credential: integration.UserCredential{UserID: 4569421795454615552, SysOrigin: "LIKEI"}, profile: integration.UserProfile{ ID: 4569421795454615552, Account: "1234567", UserNickname: "tester", UserAvatar: "https://example.com/avatar.png", }, accountMap: map[string]integration.UserProfile{ "1234567": { ID: 4569421795454615552, Account: "1234567", UserNickname: "tester", UserAvatar: "https://example.com/avatar.png", }, }, balance: 88, } service := NewGameOpenService(config.Config{ GameOpen: config.GameOpenConfig{AppKey: "game-open-test-key"}, }, nil, gateway) req := QueryUserRequest{ GameID: "101", UID: "1234567", Token: "token", RoomID: "room-1", } req.Sign = buildQueryUserSign(req, "game-open-test-key") resp := service.HandleQueryUser(context.Background(), req, "{}") if resp.ErrorCode != 0 { t.Fatalf("HandleQueryUser() error = %+v", resp) } } func TestHandleUpdateCoinAcceptsAccountUID(t *testing.T) { gateway := &stubGateway{ credential: integration.UserCredential{UserID: 4569421795454615552, SysOrigin: "LIKEI"}, accountMap: map[string]integration.UserProfile{ "1234567": { ID: 4569421795454615552, Account: "1234567", }, }, balance: 99, } service := NewGameOpenService(config.Config{ GameOpen: config.GameOpenConfig{AppKey: "game-open-test-key"}, }, nil, gateway) req := UpdateCoinRequest{ OrderID: "order-1", GameID: "101", RoundID: "round-1", UID: "1234567", Coin: 15, Type: 2, RewardType: 2, Token: "token", WinID: "win-1", RoomID: "room-1", } req.Sign = buildUpdateCoinSign(req, "game-open-test-key") resp := service.HandleUpdateCoin(context.Background(), req, "{}") if resp.ErrorCode != 0 { t.Fatalf("HandleUpdateCoin() error = %+v", resp) } } func TestHandleSupplementAcceptsAccountUID(t *testing.T) { gateway := &stubGateway{ accountMap: map[string]integration.UserProfile{ "1234567": { ID: 4569421795454615552, Account: "1234567", }, }, balance: 66, } service := NewGameOpenService(config.Config{ GameOpen: config.GameOpenConfig{AppKey: "game-open-test-key"}, }, nil, gateway) req := SupplementRequest{ OrderID: "order-1", GameID: "101", RoundID: "round-1", UID: "1234567", Coin: 1, RewardType: 1, WinID: "win-1", RoomID: "room-1", } req.Sign = buildSupplementSign(req, "game-open-test-key") resp := service.HandleSupplement(context.Background(), req, "{}") if resp.ErrorCode != 0 { t.Fatalf("HandleSupplement() error = %+v", resp) } data, ok := resp.Data.(map[string]any) if !ok { t.Fatalf("HandleSupplement() data type = %T", resp.Data) } if got := data["coin"]; got != int64(66) { t.Fatalf("HandleSupplement() coin = %v, want 66", got) } } func TestGetIntegrationInfo(t *testing.T) { gateway := &stubGateway{token: "token-123"} service := NewGameOpenService(config.Config{ GameOpen: config.GameOpenConfig{ AppKey: "game-open-test-key", TestUserID: 1234567, TestAccount: "1234567", TestPassword: "1234567", }, }, nil, gateway) info, err := service.GetIntegrationInfo(context.Background(), "https://jvapi.haiyihy.com") if err != nil { t.Fatalf("GetIntegrationInfo() error = %v", err) } if info.QueryUser != "https://jvapi.haiyihy.com/game/open/user-info" { t.Fatalf("GetIntegrationInfo() query url = %q", info.QueryUser) } if info.Token != "token-123" { t.Fatalf("GetIntegrationInfo() token = %q", info.Token) } }