package gameopen import ( "chatapp3-golang/internal/config" "chatapp3-golang/internal/integration" "chatapp3-golang/internal/model" "context" "errors" "testing" "time" "gorm.io/driver/sqlite" "gorm.io/gorm" ) type stubGateway struct { credential integration.UserCredential token string authToken string profile integration.UserProfile accountMap map[string]integration.UserProfile balance int64 exists bool changeErr error 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 s.changeErr } 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 TestCallbacksUseLingxianProviderConfigAppKey(t *testing.T) { db := newGameOpenTestDB(t) now := time.Now() if err := db.Create(&model.LingxianProviderConfig{ ID: 1, SysOrigin: "LIKEI", Profile: "PROD", Active: true, AppKey: "backend-lingxian-key", CreateTime: now, UpdateTime: now, }).Error; err != nil { t.Fatalf("create lingxian config: %v", err) } if err := db.Create(&model.SysGameListVendorExt{ ID: 2, GameListConfigID: 3, SysOrigin: "LIKEI", Profile: "PROD", VendorType: "LINGXIAN", VendorGameID: "101", Enabled: true, CreateTime: now, UpdateTime: now, }).Error; err != nil { t.Fatalf("create vendor ext: %v", err) } gateway := &stubGateway{ credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"}, profile: integration.UserProfile{ ID: 1234567, Account: "1234567", }, accountMap: map[string]integration.UserProfile{ "1234567": {ID: 1234567, Account: "1234567"}, }, balance: 99, } service := NewGameOpenService(config.Config{ GameOpen: config.GameOpenConfig{AppKey: "wrong-env-key"}, }, db, gateway) queryReq := QueryUserRequest{ GameID: "101", UID: "1234567", Token: "token", RoomID: "room-1", } queryReq.Sign = buildQueryUserSign(queryReq, "backend-lingxian-key") if resp := service.HandleQueryUser(context.Background(), queryReq, "{}"); resp.ErrorCode != 0 { t.Fatalf("HandleQueryUser() error = %+v", resp) } updateReq := 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", } updateReq.Sign = buildUpdateCoinSign(updateReq, "backend-lingxian-key") if resp := service.HandleUpdateCoin(context.Background(), updateReq, "{}"); resp.ErrorCode != 0 { t.Fatalf("HandleUpdateCoin() error = %+v", resp) } supplementReq := SupplementRequest{ OrderID: "order-2", GameID: "101", RoundID: "round-2", UID: "1234567", Coin: 1, RewardType: 1, WinID: "win-2", RoomID: "room-1", } supplementReq.Sign = buildSupplementSign(supplementReq, "backend-lingxian-key") if resp := service.HandleSupplement(context.Background(), supplementReq, "{}"); resp.ErrorCode != 0 { t.Fatalf("HandleSupplement() error = %+v", resp) } } func TestHotgameGetUserInfoUsesHotgameSign(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: "hotgame-key"}, }, nil, gateway) req := HotgameGetUserInfoRequest{ GameID: "1", UID: "1234567", Token: "token", } req.Sign = buildHotgameGetUserInfoSign(req, "hotgame-key") resp := service.HandleHotgameGetUserInfo(context.Background(), req, "{}") if resp.ErrorCode != 0 { t.Fatalf("HandleHotgameGetUserInfo() error = %+v", resp) } data, ok := resp.Data.(map[string]any) if !ok { t.Fatalf("HandleHotgameGetUserInfo() data type = %T", resp.Data) } if got := data["vipLevel"]; got != 0 { t.Fatalf("HandleHotgameGetUserInfo() vipLevel = %v, want 0", got) } } func TestHotgameUpdateBalanceAcceptsNumericRoundIDAndDuplicateReturns3001(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: "hotgame-key"}, }, nil, gateway) req := HotgameUpdateBalanceRequest{ OrderID: "order-1", GameID: "1", RoundID: float64(3199), UID: "1234567", Coin: 15, Type: 1, Token: "token", } req.Sign = buildHotgameUpdateBalanceSign(req, "hotgame-key") resp := service.HandleHotgameUpdateBalance(context.Background(), req, "{}") if resp.ErrorCode != hotgameCodeDuplicateOrder { t.Fatalf("HandleHotgameUpdateBalance() errorCode = %d, want %d", resp.ErrorCode, hotgameCodeDuplicateOrder) } if gateway.cmd.EventID != "" { t.Fatalf("HandleHotgameUpdateBalance() should not change duplicate order, got %q", gateway.cmd.EventID) } } func TestHotgameUpdateBalanceDeductFailureReturns2001(t *testing.T) { gateway := &stubGateway{ credential: integration.UserCredential{UserID: 1234567, SysOrigin: "LIKEI"}, profile: integration.UserProfile{ID: 1234567, Account: "1234567"}, balance: 99, changeErr: errors.New("insufficient_balance"), } service := NewGameOpenService(config.Config{ GameOpen: config.GameOpenConfig{AppKey: "hotgame-key"}, }, nil, gateway) req := HotgameUpdateBalanceRequest{ OrderID: "order-1", GameID: "1", RoundID: float64(3199), UID: "1234567", Coin: 15, Type: 1, Token: "token", } req.Sign = buildHotgameUpdateBalanceSign(req, "hotgame-key") resp := service.HandleHotgameUpdateBalance(context.Background(), req, "{}") if resp.ErrorCode != hotgameCodeInsufficientBalance { t.Fatalf("HandleHotgameUpdateBalance() errorCode = %d, want %d", resp.ErrorCode, hotgameCodeInsufficientBalance) } } func newGameOpenTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate( &model.LingxianProviderConfig{}, &model.HotgameProviderConfig{}, &model.SysGameListVendorExt{}, &model.GameOpenCallbackLog{}, ); err != nil { t.Fatalf("migrate sqlite: %v", err) } return db } 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) } }