1888 lines
76 KiB
Go
1888 lines
76 KiB
Go
package game
|
|
|
|
import (
|
|
"context"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/md5"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
jwt "github.com/golang-jwt/jwt/v5"
|
|
"google.golang.org/grpc"
|
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
|
gamev1 "hyapp.local/api/proto/game/v1"
|
|
userv1 "hyapp.local/api/proto/user/v1"
|
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
|
"hyapp/pkg/xerr"
|
|
gamedomain "hyapp/services/game-service/internal/domain/game"
|
|
)
|
|
|
|
func TestLaunchGameCreatesSessionAndRejectsMaintenance(t *testing.T) {
|
|
repo := &fakeRepository{
|
|
launchable: gamedomain.LaunchableGame{
|
|
CatalogItem: gamedomain.CatalogItem{
|
|
AppCode: "lalu",
|
|
GameID: "demo_rocket_001",
|
|
PlatformCode: "demo",
|
|
ProviderGameID: "rocket_001",
|
|
GameName: "Rocket",
|
|
Status: gamedomain.StatusActive,
|
|
Orientation: "portrait",
|
|
},
|
|
PlatformStatus: gamedomain.StatusActive,
|
|
APIBaseURL: "https://provider.example/h5",
|
|
},
|
|
}
|
|
svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
result, err := svc.LaunchGame(context.Background(), LaunchCommand{
|
|
AppCode: "lalu",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
GameID: "demo_rocket_001",
|
|
Scene: gamedomain.SceneVoiceRoom,
|
|
RoomID: "room_1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LaunchGame failed: %v", err)
|
|
}
|
|
if result.SessionID == "" || result.ExpiresAtMS != 1700000900000 || !strings.Contains(result.LaunchURL, "session_token=") {
|
|
t.Fatalf("launch result mismatch: %+v", result)
|
|
}
|
|
if repo.session.GameID != "demo_rocket_001" || repo.session.UserID != 42 || repo.session.RegionID != 100 || repo.session.LaunchTokenHash == "" {
|
|
t.Fatalf("launch session was not persisted: %+v", repo.session)
|
|
}
|
|
|
|
repo.launchable.PlatformStatus = gamedomain.StatusMaintenance
|
|
_, err = svc.LaunchGame(context.Background(), LaunchCommand{AppCode: "lalu", UserID: 42, GameID: "demo_rocket_001"})
|
|
if !xerr.IsCode(err, xerr.Conflict) {
|
|
t.Fatalf("maintenance game should reject launch, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListRecentGamesNormalizesQueryAndDefaultsLimit(t *testing.T) {
|
|
repo := &fakeRepository{
|
|
recentGames: []gamedomain.AppGame{
|
|
{GameID: "viva_2", Name: "Rich Forever"},
|
|
},
|
|
}
|
|
svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
games, serverTimeMS, err := svc.ListRecentGames(context.Background(), ListRecentGamesQuery{
|
|
UserID: 42,
|
|
Scene: "",
|
|
Language: " EN-US ",
|
|
ClientPlatform: " Android ",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ListRecentGames failed: %v", err)
|
|
}
|
|
if serverTimeMS != 1700000000000 || len(games) != 1 || games[0].GameID != "viva_2" {
|
|
t.Fatalf("recent games response mismatch: time=%d games=%+v", serverTimeMS, games)
|
|
}
|
|
if repo.lastRecentQuery.AppCode != "lalu" || repo.lastRecentQuery.Scene != gamedomain.SceneVoiceRoom || repo.lastRecentQuery.PageSize != 4 || repo.lastRecentQuery.Language != "en-us" || repo.lastRecentQuery.ClientPlatform != "android" {
|
|
t.Fatalf("recent query normalization mismatch: %+v", repo.lastRecentQuery)
|
|
}
|
|
}
|
|
|
|
func TestGetBridgeScriptUsesFirstActivePlatformConfig(t *testing.T) {
|
|
repo := &fakeRepository{
|
|
platforms: []gamedomain.Platform{
|
|
{
|
|
AppCode: "lalu",
|
|
PlatformCode: "vivagames",
|
|
Status: gamedomain.StatusActive,
|
|
SortOrder: 20,
|
|
AdapterConfigJSON: `{"bridge_script_url":"https://cdn.example.test/bridge-viva.js","bridge_script_version":"20260608.2","bridge_script_sha256":"def"}`,
|
|
},
|
|
{
|
|
AppCode: "lalu",
|
|
PlatformCode: "baishun",
|
|
Status: gamedomain.StatusActive,
|
|
SortOrder: 10,
|
|
AdapterConfigJSON: `{"bridgeScriptUrl":"https://cdn.example.test/bridge.js","bridgeScriptVersion":"20260608.1","bridgeScriptSha256":"abc"}`,
|
|
},
|
|
},
|
|
}
|
|
svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
result, err := svc.GetBridgeScript(context.Background(), " LALU ")
|
|
if err != nil {
|
|
t.Fatalf("GetBridgeScript failed: %v", err)
|
|
}
|
|
if result.URL != "https://cdn.example.test/bridge.js" || result.Version != "20260608.1" || result.SHA256 != "abc" || result.ServerTimeMS != 1700000000000 {
|
|
t.Fatalf("bridge script mismatch: %+v", result)
|
|
}
|
|
if repo.lastListPlatformsAppCode != "lalu" || repo.lastListPlatformsStatus != gamedomain.StatusActive {
|
|
t.Fatalf("ListPlatforms query mismatch: app=%q status=%q", repo.lastListPlatformsAppCode, repo.lastListPlatformsStatus)
|
|
}
|
|
}
|
|
|
|
func TestLaunchGameBuildsYomiAuthURL(t *testing.T) {
|
|
repo := &fakeRepository{
|
|
launchable: gamedomain.LaunchableGame{
|
|
CatalogItem: gamedomain.CatalogItem{
|
|
AppCode: "lalu",
|
|
GameID: "yomi_fish_001",
|
|
PlatformCode: "yomi",
|
|
ProviderGameID: "33",
|
|
GameName: "Fish",
|
|
Status: gamedomain.StatusActive,
|
|
Orientation: "landscape",
|
|
},
|
|
PlatformStatus: gamedomain.StatusActive,
|
|
APIBaseURL: "https://api-testv2.yomigame.games/gate/gateway/auth",
|
|
AdapterType: gamedomain.AdapterYomiV4,
|
|
AdapterConfigJSON: `{"default_lang":"en","token_ttl_seconds":86400,"uid_mode":"user_id","game_level_uids":{"33":17},"plat_payload":"{\"env\":\"test\"}"}`,
|
|
},
|
|
}
|
|
svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
result, err := svc.LaunchGame(context.Background(), LaunchCommand{
|
|
AppCode: "lalu",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
GameID: "yomi_fish_001",
|
|
RoomID: "room_1",
|
|
AccessToken: "app_access_token_yomi",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LaunchGame failed: %v", err)
|
|
}
|
|
if result.ExpiresAtMS != 1700086400000 {
|
|
t.Fatalf("yomi launch must use adapter token ttl, got %+v", result)
|
|
}
|
|
parsed, err := url.Parse(result.LaunchURL)
|
|
if err != nil {
|
|
t.Fatalf("parse yomi launch url failed: %v", err)
|
|
}
|
|
query := parsed.Query()
|
|
expected := map[string]string{
|
|
"gameUid": "33",
|
|
"platUserId": "42",
|
|
"lang": "en",
|
|
"gameLevelUid": "17",
|
|
"platRoomId": "room_1",
|
|
"platAuthCode": "app_access_token_yomi",
|
|
"platPayload": `{"env":"test"}`,
|
|
}
|
|
for key, want := range expected {
|
|
if got := query.Get(key); got != want {
|
|
t.Fatalf("yomi launch query %s mismatch: got %q want %q url=%s", key, got, want, result.LaunchURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLaunchGameRequiresYomiAuthURL(t *testing.T) {
|
|
repo := &fakeRepository{
|
|
launchable: gamedomain.LaunchableGame{
|
|
CatalogItem: gamedomain.CatalogItem{
|
|
AppCode: "lalu",
|
|
GameID: "yomi_fish_001",
|
|
PlatformCode: "yomi",
|
|
ProviderGameID: "33",
|
|
GameName: "Fish",
|
|
Status: gamedomain.StatusActive,
|
|
},
|
|
PlatformStatus: gamedomain.StatusActive,
|
|
AdapterType: gamedomain.AdapterYomiV4,
|
|
AdapterConfigJSON: `{"default_lang":"en"}`,
|
|
},
|
|
}
|
|
svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
_, err := svc.LaunchGame(context.Background(), LaunchCommand{
|
|
AppCode: "lalu",
|
|
UserID: 42,
|
|
GameID: "yomi_fish_001",
|
|
AccessToken: "app_access_token_yomi",
|
|
})
|
|
if err == nil || !strings.Contains(xerr.MessageOf(err), "yomi auth url is required") {
|
|
t.Fatalf("LaunchGame must reject missing yomi auth url, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLaunchGameBuildsLeaderCCURL(t *testing.T) {
|
|
repo := &fakeRepository{
|
|
launchable: gamedomain.LaunchableGame{
|
|
CatalogItem: gamedomain.CatalogItem{
|
|
AppCode: "lalu",
|
|
GameID: "lingxian_rocket_001",
|
|
PlatformCode: "lingxian",
|
|
ProviderGameID: "101",
|
|
GameName: "Rocket",
|
|
Status: gamedomain.StatusActive,
|
|
Orientation: "portrait",
|
|
},
|
|
PlatformStatus: gamedomain.StatusActive,
|
|
APIBaseURL: "https://games.leadercc.com/test/index.html",
|
|
AdapterType: gamedomain.AdapterLeaderCCV1,
|
|
AdapterConfigJSON: `{"default_lang":"zh-CN","token_ttl_seconds":86400,"uid_mode":"display_user_id"}`,
|
|
},
|
|
}
|
|
svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
result, err := svc.LaunchGame(context.Background(), LaunchCommand{
|
|
AppCode: "lalu",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
GameID: "lingxian_rocket_001",
|
|
RoomID: "room_1",
|
|
AccessToken: "app_access_token_lingxian",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LaunchGame failed: %v", err)
|
|
}
|
|
if result.ExpiresAtMS != 1700086400000 {
|
|
t.Fatalf("leadercc launch must use adapter token ttl, got %+v", result)
|
|
}
|
|
for _, part := range []string{"uid=420001", "token=app_access_token_lingxian", "lang=zh-CN", "roomid=room_1"} {
|
|
if !strings.Contains(result.LaunchURL, part) {
|
|
t.Fatalf("leadercc launch url missing %s: %s", part, result.LaunchURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLaunchGameBuildsZeeOneURL(t *testing.T) {
|
|
repo := &fakeRepository{
|
|
launchable: gamedomain.LaunchableGame{
|
|
CatalogItem: gamedomain.CatalogItem{
|
|
AppCode: "lalu",
|
|
GameID: "zeeone_gold_olympus",
|
|
PlatformCode: "zeeone",
|
|
ProviderGameID: "1021",
|
|
GameName: "Gold of Olympus",
|
|
Status: gamedomain.StatusActive,
|
|
Orientation: "portrait",
|
|
},
|
|
PlatformStatus: gamedomain.StatusActive,
|
|
APIBaseURL: "https://fallback.example.invalid/game.html",
|
|
AdapterType: gamedomain.AdapterZeeOneV1,
|
|
AdapterConfigJSON: `{"merchant_id":307715,"platform_id":2032,"default_lang":"en","token_ttl_seconds":86400,"game_urls":{"1021":"https://dx3kkv99b7clg.cloudfront.net/test/1021_gold_of_olympus/index.html?merchant=0&platform=0"}}`,
|
|
},
|
|
}
|
|
svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
result, err := svc.LaunchGame(context.Background(), LaunchCommand{
|
|
AppCode: "lalu",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
GameID: "zeeone_gold_olympus",
|
|
RoomID: "01",
|
|
AccessToken: "app_access_token_zeeone",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LaunchGame failed: %v", err)
|
|
}
|
|
if result.ExpiresAtMS != 1700086400000 {
|
|
t.Fatalf("zeeone launch must use adapter token ttl, got %+v", result)
|
|
}
|
|
if !strings.HasPrefix(result.LaunchURL, "https://dx3kkv99b7clg.cloudfront.net/test/1021_gold_of_olympus/index.html?") {
|
|
t.Fatalf("zeeone launch url must use per-game configured url: %s", result.LaunchURL)
|
|
}
|
|
for _, part := range []string{"sessionId=app_access_token_zeeone", "language=en", "merchant=307715", "platform=2032", "sid=01"} {
|
|
if !strings.Contains(result.LaunchURL, part) {
|
|
t.Fatalf("zeeone launch url missing %s: %s", part, result.LaunchURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLaunchGameRequiresZeeOneLaunchURL(t *testing.T) {
|
|
repo := &fakeRepository{
|
|
launchable: gamedomain.LaunchableGame{
|
|
CatalogItem: gamedomain.CatalogItem{
|
|
AppCode: "lalu",
|
|
GameID: "zeeone_unknown",
|
|
PlatformCode: "zeeone",
|
|
ProviderGameID: "9999",
|
|
GameName: "Unknown ZeeOne Game",
|
|
Status: gamedomain.StatusActive,
|
|
},
|
|
PlatformStatus: gamedomain.StatusActive,
|
|
AdapterType: gamedomain.AdapterZeeOneV1,
|
|
AdapterConfigJSON: `{"merchant_id":307715,"platform_id":2032}`,
|
|
},
|
|
}
|
|
svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
_, err := svc.LaunchGame(context.Background(), LaunchCommand{
|
|
AppCode: "lalu",
|
|
UserID: 42,
|
|
GameID: "zeeone_unknown",
|
|
AccessToken: "app_access_token_zeeone",
|
|
})
|
|
if err == nil || !strings.Contains(xerr.MessageOf(err), "zeeone launch url is required") {
|
|
t.Fatalf("LaunchGame must reject missing zeeone launch url, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLaunchGameBuildsBaishunURL(t *testing.T) {
|
|
repo := &fakeRepository{
|
|
launchable: gamedomain.LaunchableGame{
|
|
CatalogItem: gamedomain.CatalogItem{
|
|
AppCode: "lalu",
|
|
GameID: "baishun_greedy",
|
|
PlatformCode: "baishun",
|
|
ProviderGameID: "1006",
|
|
GameName: "Greedy",
|
|
Status: gamedomain.StatusActive,
|
|
Orientation: "portrait",
|
|
},
|
|
PlatformStatus: gamedomain.StatusActive,
|
|
APIBaseURL: "https://fallback.example.invalid/game.html",
|
|
AdapterType: gamedomain.AdapterBaishunV1,
|
|
AdapterConfigJSON: `{"app_id":21397507,"app_channel":"mesh","default_lang":"2","token_ttl_seconds":86400,"game_mode":"3","scene_mode":1,"currency_icon":"https://cdn.example/coin.png","gsp":101,"game_urls":{"1006":"https://game-cn-test.jieyou.shop/game-packages/greedy/index.html"}}`,
|
|
},
|
|
}
|
|
svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
result, err := svc.LaunchGame(context.Background(), LaunchCommand{
|
|
AppCode: "lalu",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
GameID: "baishun_greedy",
|
|
RoomID: "room_1",
|
|
AccessToken: "app_access_token_baishun",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LaunchGame failed: %v", err)
|
|
}
|
|
if result.ExpiresAtMS != 1700086400000 {
|
|
t.Fatalf("baishun launch must use adapter token ttl, got %+v", result)
|
|
}
|
|
parsed, err := url.Parse(result.LaunchURL)
|
|
if err != nil {
|
|
t.Fatalf("parse baishun launch url failed: %v", err)
|
|
}
|
|
if parsed.Scheme+"://"+parsed.Host+parsed.Path != "https://game-cn-test.jieyou.shop/game-packages/greedy/index.html" {
|
|
t.Fatalf("baishun launch url must use per-game configured url: %s", result.LaunchURL)
|
|
}
|
|
query := parsed.Query()
|
|
expected := map[string]string{
|
|
"appChannel": "mesh",
|
|
"appId": "21397507",
|
|
"userId": "42",
|
|
"code": "app_access_token_baishun",
|
|
"roomId": "room_1",
|
|
"gameMode": "3",
|
|
"language": "2",
|
|
"sceneMode": "1",
|
|
"isDataByUrl": "1",
|
|
"bridge_keys": "appChannel,appId,userId,code,gameMode,gsp,language,roomId,sceneMode,currencyIcon,isDataByUrl",
|
|
"currencyIcon": "https://cdn.example/coin.png",
|
|
"gsp": "101",
|
|
}
|
|
for key, want := range expected {
|
|
if got := query.Get(key); got != want {
|
|
t.Fatalf("baishun launch query %s mismatch: got %q want %q url=%s", key, got, want, result.LaunchURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLaunchGameBuildsVivaGamesURL(t *testing.T) {
|
|
repo := &fakeRepository{
|
|
launchable: gamedomain.LaunchableGame{
|
|
CatalogItem: gamedomain.CatalogItem{
|
|
AppCode: "lalu",
|
|
GameID: "vivagames_2",
|
|
PlatformCode: "vivagames",
|
|
ProviderGameID: "2",
|
|
GameName: "Viva Demo",
|
|
Status: gamedomain.StatusActive,
|
|
Orientation: "portrait",
|
|
},
|
|
PlatformStatus: gamedomain.StatusActive,
|
|
APIBaseURL: "https://fallback.example.invalid/game.html",
|
|
AdapterType: gamedomain.AdapterVivaGamesV1,
|
|
AdapterConfigJSON: `{
|
|
"app_id":1012127974,
|
|
"default_lang":"en-US",
|
|
"token_ttl_seconds":86400,
|
|
"uid_mode":"display_user_id",
|
|
"amount_rate":1,
|
|
"bgm_switch":0,
|
|
"gsp":101,
|
|
"currency_icon":"https://cdn.example/coin.png",
|
|
"bridge_script_url":"https://cdn.example/game-bridge/voice-room-game-bridge.v1.js",
|
|
"bridge_script_version":"20260608.1",
|
|
"bridge_script_sha256":"78f0d2d0b52d5711c1f2a9e8d3af8a9d3db7e03c83f5bd8d2b8e629f1f5b6c13",
|
|
"game_urls":{"2":"https://dev-rich2.s3.ap-southeast-1.amazonaws.com/richForever.html?game_id=2&isShow=1"}
|
|
}`,
|
|
},
|
|
}
|
|
svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
result, err := svc.LaunchGame(context.Background(), LaunchCommand{
|
|
AppCode: "lalu",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
GameID: "vivagames_2",
|
|
RoomID: "room_1",
|
|
AccessToken: "app_access_token_viva",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LaunchGame failed: %v", err)
|
|
}
|
|
if result.ExpiresAtMS != 1700086400000 {
|
|
t.Fatalf("vivagames launch must use adapter token ttl, got %+v", result)
|
|
}
|
|
parsed, err := url.Parse(result.LaunchURL)
|
|
if err != nil {
|
|
t.Fatalf("parse vivagames launch url failed: %v", err)
|
|
}
|
|
if parsed.Scheme+"://"+parsed.Host+parsed.Path != "https://dev-rich2.s3.ap-southeast-1.amazonaws.com/richForever.html" {
|
|
t.Fatalf("vivagames launch url must use per-game configured url: %s", result.LaunchURL)
|
|
}
|
|
query := parsed.Query()
|
|
expected := map[string]string{
|
|
"appId": "1012127974",
|
|
"userId": "420001",
|
|
"code": "app_access_token_viva",
|
|
"gameId": "2",
|
|
"metadata": `{"room_id":"room_1"}`,
|
|
"language": "en-US",
|
|
"coinType": "0",
|
|
"amountRate": "1",
|
|
"bgmSwitch": "0",
|
|
"gsp": "101",
|
|
"currencyIcon": "https://cdn.example/coin.png",
|
|
"gameConfig": `{"currencyIcon":"https://cdn.example/coin.png"}`,
|
|
"bridge_keys": "appId,userId,code,metadata,language,coinType,amountRate,bgmSwitch,gsp,currencyIcon,gameConfig,gameId,bridge_script_url,bridge_script_version,bridge_script_sha256",
|
|
"bridge_script_url": "https://cdn.example/game-bridge/voice-room-game-bridge.v1.js",
|
|
"bridgeScriptUrl": "https://cdn.example/game-bridge/voice-room-game-bridge.v1.js",
|
|
"bridge_script_version": "20260608.1",
|
|
"bridgeScriptVersion": "20260608.1",
|
|
"bridge_script_sha256": "78f0d2d0b52d5711c1f2a9e8d3af8a9d3db7e03c83f5bd8d2b8e629f1f5b6c13",
|
|
"bridgeScriptSha256": "78f0d2d0b52d5711c1f2a9e8d3af8a9d3db7e03c83f5bd8d2b8e629f1f5b6c13",
|
|
}
|
|
for key, want := range expected {
|
|
if got := query.Get(key); got != want {
|
|
t.Fatalf("vivagames launch query %s mismatch: got %q want %q url=%s", key, got, want, result.LaunchURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLaunchGameBuildsReyouURL(t *testing.T) {
|
|
repo := &fakeRepository{
|
|
launchable: gamedomain.LaunchableGame{
|
|
CatalogItem: gamedomain.CatalogItem{
|
|
AppCode: "lalu",
|
|
GameID: "reyou_101",
|
|
PlatformCode: "reyou",
|
|
ProviderGameID: "101",
|
|
GameName: "Reyou Demo",
|
|
Status: gamedomain.StatusActive,
|
|
Orientation: "portrait",
|
|
},
|
|
PlatformStatus: gamedomain.StatusActive,
|
|
APIBaseURL: "https://fallback.example.invalid/game.html",
|
|
AdapterType: gamedomain.AdapterReyouV1,
|
|
AdapterConfigJSON: `{
|
|
"default_lang":"en",
|
|
"token_ttl_seconds":86400,
|
|
"uid_mode":"display_user_id",
|
|
"bridge_script_url":"https://cdn.example/game-bridge/voice-room-game-bridge.v1.js",
|
|
"bridge_script_version":"20260608.1",
|
|
"bridge_script_sha256":"78f0d2d0b52d5711c1f2a9e8d3af8a9d3db7e03c83f5bd8d2b8e629f1f5b6c13",
|
|
"game_urls":{"101":"https://games.reyou.example/test/box/half/?game_id=101"}
|
|
}`,
|
|
},
|
|
}
|
|
svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
result, err := svc.LaunchGame(context.Background(), LaunchCommand{
|
|
AppCode: "lalu",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
GameID: "reyou_101",
|
|
RoomID: "room_1",
|
|
AccessToken: "app_access_token_reyou",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LaunchGame failed: %v", err)
|
|
}
|
|
if result.ExpiresAtMS != 1700086400000 {
|
|
t.Fatalf("reyou launch must use adapter token ttl, got %+v", result)
|
|
}
|
|
parsed, err := url.Parse(result.LaunchURL)
|
|
if err != nil {
|
|
t.Fatalf("parse reyou launch url failed: %v", err)
|
|
}
|
|
if parsed.Scheme+"://"+parsed.Host+parsed.Path != "https://games.reyou.example/test/box/half/" {
|
|
t.Fatalf("reyou launch url must use per-game configured url: %s", result.LaunchURL)
|
|
}
|
|
expected := map[string]string{
|
|
"game_id": "101",
|
|
"uid": "420001",
|
|
"token": "app_access_token_reyou",
|
|
"lang": "en",
|
|
"bridge_script_url": "https://cdn.example/game-bridge/voice-room-game-bridge.v1.js",
|
|
"bridge_script_version": "20260608.1",
|
|
"bridge_script_sha256": "78f0d2d0b52d5711c1f2a9e8d3af8a9d3db7e03c83f5bd8d2b8e629f1f5b6c13",
|
|
}
|
|
for key, want := range expected {
|
|
if got := parsed.Query().Get(key); got != want {
|
|
t.Fatalf("reyou launch query %s mismatch: got %q want %q url=%s", key, got, want, result.LaunchURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLaunchGameRequiresBaishunLaunchURL(t *testing.T) {
|
|
repo := &fakeRepository{
|
|
launchable: gamedomain.LaunchableGame{
|
|
CatalogItem: gamedomain.CatalogItem{
|
|
AppCode: "lalu",
|
|
GameID: "baishun_unknown",
|
|
PlatformCode: "baishun",
|
|
ProviderGameID: "9999",
|
|
GameName: "Unknown Baishun Game",
|
|
Status: gamedomain.StatusActive,
|
|
},
|
|
PlatformStatus: gamedomain.StatusActive,
|
|
AdapterType: gamedomain.AdapterBaishunV1,
|
|
AdapterConfigJSON: `{"app_id":21397507,"app_channel":"mesh"}`,
|
|
},
|
|
}
|
|
svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
_, err := svc.LaunchGame(context.Background(), LaunchCommand{
|
|
AppCode: "lalu",
|
|
UserID: 42,
|
|
GameID: "baishun_unknown",
|
|
AccessToken: "app_access_token_baishun",
|
|
})
|
|
if err == nil || !strings.Contains(xerr.MessageOf(err), "baishun launch url is required") {
|
|
t.Fatalf("LaunchGame must reject missing baishun launch url, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLaunchGameAllowsLeaderCCWithoutLaunchURL(t *testing.T) {
|
|
repo := &fakeRepository{
|
|
launchable: gamedomain.LaunchableGame{
|
|
CatalogItem: gamedomain.CatalogItem{
|
|
AppCode: "lalu",
|
|
GameID: "lingxian_rocket_001",
|
|
PlatformCode: "lingxian",
|
|
ProviderGameID: "101",
|
|
GameName: "Rocket",
|
|
Status: gamedomain.StatusActive,
|
|
},
|
|
PlatformStatus: gamedomain.StatusActive,
|
|
AdapterType: gamedomain.AdapterLeaderCCV1,
|
|
AdapterConfigJSON: `{"token_ttl_seconds":86400}`,
|
|
},
|
|
}
|
|
svc := New(Config{LaunchSessionTTL: 15 * time.Minute}, repo, &fakeWallet{}, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
result, err := svc.LaunchGame(context.Background(), LaunchCommand{
|
|
AppCode: "lalu",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
GameID: "lingxian_rocket_001",
|
|
AccessToken: "app_access_token_lingxian",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LaunchGame failed: %v", err)
|
|
}
|
|
if result.LaunchURL != "" || repo.session.LaunchTokenHash != stableHash("app_access_token_lingxian") {
|
|
t.Fatalf("leadercc without launch url should only create session: result=%+v session=%+v", result, repo.session)
|
|
}
|
|
}
|
|
|
|
func TestHandleLeaderCCUserInfoValidatesSignAndToken(t *testing.T) {
|
|
key := "leadercc-test-key"
|
|
token := "gt_lingxian_token"
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: "lingxian",
|
|
AdapterType: gamedomain.AdapterLeaderCCV1,
|
|
CallbackSecretCiphertext: key,
|
|
CallbackIPWhitelist: []string{"3.1.174.194"},
|
|
AdapterConfigJSON: `{"uid_mode":"display_user_id"}`,
|
|
},
|
|
session: gamedomain.LaunchSession{
|
|
AppCode: "lalu",
|
|
SessionID: "game_sess_lingxian",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
RoomID: "room_1",
|
|
PlatformCode: "lingxian",
|
|
GameID: "lingxian_rocket_001",
|
|
ProviderGameID: "101",
|
|
LaunchTokenHash: stableHash(token),
|
|
Status: gamedomain.SessionActive,
|
|
ExpiresAtMS: 1700086400000,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 1680}
|
|
svc := New(Config{}, repo, wallet, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
sign := leaderccTestSign(key, "101", "420001", token, "room_1")
|
|
raw, contentType, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-leadercc-user", AppCode: "lalu"},
|
|
PlatformCode: "lingxian",
|
|
Operation: "userinfo",
|
|
RawBody: []byte(`{"gameId":"101","uid":"420001","token":"` + token + `","roomId":"room_1","sign":"` + sign + `"}`),
|
|
RemoteAddr: "203.0.113.10:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback failed: %v", err)
|
|
}
|
|
if contentType != "application/json" || !strings.Contains(string(raw), `"errorCode":0`) || !strings.Contains(string(raw), `"coin":1680`) {
|
|
t.Fatalf("leadercc user response mismatch: %s", raw)
|
|
}
|
|
}
|
|
|
|
func TestHandleLeaderCCUserInfoAcceptsAppAccessTokenWithoutLaunchSession(t *testing.T) {
|
|
key := "leadercc-test-key"
|
|
token := leaderccTestAccessToken(t, "lalu", 42, "420001", 1700003600)
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: "lingxian",
|
|
AdapterType: gamedomain.AdapterLeaderCCV1,
|
|
CallbackSecretCiphertext: key,
|
|
AdapterConfigJSON: `{"uid_mode":"display_user_id"}`,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 1680}
|
|
user := &fakeUser{}
|
|
svc := New(Config{}, repo, wallet, user)
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
sign := leaderccTestSign(key, "101", "420001", token, "room_1")
|
|
raw, contentType, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-leadercc-app-token-user", AppCode: "lalu"},
|
|
PlatformCode: "lingxian",
|
|
Operation: "userinfo",
|
|
RawBody: []byte(`{"gameId":"101","uid":"420001","token":"` + token + `","roomId":"room_1","sign":"` + sign + `"}`),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback failed: %v", err)
|
|
}
|
|
if contentType != "application/json" || !strings.Contains(string(raw), `"code":0`) || !strings.Contains(string(raw), `"errorCode":0`) || !strings.Contains(string(raw), `"coin":1680`) {
|
|
t.Fatalf("leadercc app-token user response mismatch: %s", raw)
|
|
}
|
|
if user.lastGet.GetUserId() != 42 {
|
|
t.Fatalf("user request must use token user_id, got %+v", user.lastGet)
|
|
}
|
|
}
|
|
|
|
func TestHandleLeaderCCChangeCoinAndRepairOrder(t *testing.T) {
|
|
key := "leadercc-test-key"
|
|
token := "gt_lingxian_token"
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: "lingxian",
|
|
AdapterType: gamedomain.AdapterLeaderCCV1,
|
|
CallbackSecretCiphertext: key,
|
|
AdapterConfigJSON: `{"uid_mode":"user_id"}`,
|
|
},
|
|
session: gamedomain.LaunchSession{
|
|
AppCode: "lalu",
|
|
SessionID: "game_sess_lingxian",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
RoomID: "room_1",
|
|
PlatformCode: "lingxian",
|
|
GameID: "lingxian_rocket_001",
|
|
ProviderGameID: "101",
|
|
LaunchTokenHash: stableHash(token),
|
|
Status: gamedomain.SessionActive,
|
|
ExpiresAtMS: 1700086400000,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 1560}
|
|
svc := New(Config{}, repo, wallet, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
changeSign := leaderccTestSign(key, "order_1", "101", "round_1", "42", "120", "1", "2", token, "", "room_1")
|
|
raw, _, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-leadercc-change", AppCode: "lalu"},
|
|
PlatformCode: "lingxian",
|
|
Operation: "change_coin",
|
|
RawBody: []byte(`{"orderId":"order_1","gameId":"101","roundId":"round_1","uid":"42","coin":120,"type":1,"rewardType":2,"token":"` + token + `","winId":"","roomId":"room_1","sign":"` + changeSign + `"}`),
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"errorCode":0`) || !strings.Contains(string(raw), `"coin":1560`) {
|
|
t.Fatalf("leadercc change response mismatch: %s", raw)
|
|
}
|
|
if wallet.lastApply.GetCommandId() != "game:lingxian:order_1" || wallet.lastApply.GetOpType() != "debit" || wallet.lastApply.GetCoinAmount() != 120 {
|
|
t.Fatalf("wallet command mismatch: %+v", wallet.lastApply)
|
|
}
|
|
|
|
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-leadercc-change-replay", AppCode: "lalu"},
|
|
PlatformCode: "lingxian",
|
|
Operation: "change_coin",
|
|
RawBody: []byte(`{"orderId":"order_1","gameId":"101","roundId":"round_1","uid":"42","coin":120,"type":1,"rewardType":2,"token":"` + token + `","winId":"","roomId":"room_1","sign":"` + changeSign + `"}`),
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("duplicate HandleCallback failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"errorCode":10003`) || !strings.Contains(string(raw), `"code":10003`) || !strings.Contains(string(raw), `"coin":1560`) {
|
|
t.Fatalf("leadercc duplicate change response mismatch: %s", raw)
|
|
}
|
|
|
|
repairSign := leaderccTestSign(key, "repair_1", "101", "round_2", "42", "50", "2", "", "room_1")
|
|
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-leadercc-repair", AppCode: "lalu"},
|
|
PlatformCode: "lingxian",
|
|
Operation: "repair_order",
|
|
RawBody: []byte(`{"orderId":"repair_1","gameId":"101","roundId":"round_2","uid":"42","coin":50,"rewardType":2,"winId":"","roomId":"room_1","sign":"` + repairSign + `"}`),
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"errorCode":0`) {
|
|
t.Fatalf("repair response mismatch: %s", raw)
|
|
}
|
|
if repo.repair.ProviderOrderID != "repair_1" || repo.repair.Reason != "leadercc_repair_order" {
|
|
t.Fatalf("repair order mismatch: %+v", repo.repair)
|
|
}
|
|
}
|
|
|
|
func TestHandleLeaderCCChangeCoinAcceptsAppAccessTokenAndTokenOnlyProbe(t *testing.T) {
|
|
key := "leadercc-test-key"
|
|
token := leaderccTestAccessToken(t, "lalu", 42, "420001", 1700003600)
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: "lingxian",
|
|
AdapterType: gamedomain.AdapterLeaderCCV1,
|
|
CallbackSecretCiphertext: key,
|
|
AdapterConfigJSON: `{"uid_mode":"display_user_id"}`,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 1560}
|
|
svc := New(Config{}, repo, wallet, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
probeRaw, _, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-leadercc-token-probe", AppCode: "lalu"},
|
|
PlatformCode: "lingxian",
|
|
Operation: "change_coin",
|
|
RawBody: []byte(`{"token":"` + token + `"}`),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("token-only probe failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(probeRaw), `"code":0`) || wallet.lastApply != nil {
|
|
t.Fatalf("token-only probe must return ok without applying wallet: raw=%s wallet=%+v", probeRaw, wallet.lastApply)
|
|
}
|
|
|
|
changeSign := leaderccTestSign(key, "order_app_token_1", "101", "round_1", "420001", "120", "1", "2", token, "", "room_1")
|
|
raw, _, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-leadercc-app-token-change", AppCode: "lalu"},
|
|
PlatformCode: "lingxian",
|
|
Operation: "change_coin",
|
|
RawBody: []byte(`{"orderId":"order_app_token_1","gameId":"101","roundId":"round_1","uid":"420001","coin":120,"type":1,"rewardType":2,"token":"` + token + `","winId":"","roomId":"room_1","sign":"` + changeSign + `"}`),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"code":0`) || !strings.Contains(string(raw), `"coin":1560`) {
|
|
t.Fatalf("leadercc app-token change response mismatch: %s", raw)
|
|
}
|
|
if wallet.lastApply.GetUserId() != 42 || wallet.lastApply.GetGameId() != "101" || wallet.lastApply.GetCommandId() != "game:lingxian:order_app_token_1" {
|
|
t.Fatalf("wallet command must use token user and request game: %+v", wallet.lastApply)
|
|
}
|
|
}
|
|
|
|
func TestHandleYomiGetTokenAndUserInfo(t *testing.T) {
|
|
secret := "kEdOjdxgOUCJjG1L6kl3fYEIgMFxQOza"
|
|
token := "app_access_token_yomi"
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: "yomi",
|
|
AdapterType: gamedomain.AdapterYomiV4,
|
|
CallbackSecretCiphertext: secret,
|
|
AdapterConfigJSON: `{"uid_mode":"display_user_id"}`,
|
|
},
|
|
session: gamedomain.LaunchSession{
|
|
AppCode: "lalu",
|
|
SessionID: "game_sess_yomi",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
RoomID: "room_1",
|
|
PlatformCode: "yomi",
|
|
GameID: "yomi_fish_001",
|
|
ProviderGameID: "33",
|
|
LaunchTokenHash: stableHash(token),
|
|
Status: gamedomain.SessionActive,
|
|
ExpiresAtMS: 1700086400000,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 1880}
|
|
user := &fakeUser{}
|
|
svc := New(Config{}, repo, wallet, user)
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
getTokenBody := encryptYomiTestBody(t, secret, `{"gameUid":33,"lang":"en","platAuthCode":"app_access_token_yomi","platUserId":"420001","platPayload":"","platRoomId":"room_1"}`)
|
|
raw, contentType, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-yomi-gettoken", AppCode: "lalu"},
|
|
PlatformCode: "yomi",
|
|
Operation: "gettoken",
|
|
RawBody: getTokenBody,
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback gettoken failed: %v", err)
|
|
}
|
|
if contentType != "application/json" || !strings.Contains(string(raw), `"code":200`) || !strings.Contains(string(raw), `"platToken":"app_access_token_yomi"`) || !strings.Contains(string(raw), `"balance":1880`) {
|
|
t.Fatalf("yomi gettoken response mismatch: %s", raw)
|
|
}
|
|
if user.lastGet.GetUserId() != 42 {
|
|
t.Fatalf("yomi gettoken must query token user, got %+v", user.lastGet)
|
|
}
|
|
|
|
userInfoBody := encryptYomiTestBody(t, secret, `{"platUserId":"420001","platToken":"app_access_token_yomi","platPayload":"","platRoomId":"room_1"}`)
|
|
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-yomi-userinfo", AppCode: "lalu"},
|
|
PlatformCode: "yomi",
|
|
Operation: "userinfo",
|
|
RawBody: userInfoBody,
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback userinfo failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"code":200`) || !strings.Contains(string(raw), `"platUserId":"420001"`) || !strings.Contains(string(raw), `"avatar":"https://cdn.example/avatar.png"`) {
|
|
t.Fatalf("yomi userinfo response mismatch: %s", raw)
|
|
}
|
|
}
|
|
|
|
func TestHandleYomiChangeBalanceDecryptsAndAppliesWallet(t *testing.T) {
|
|
secret := "12345678901234567890123456789012"
|
|
token := "gt_test_token"
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: "yomi",
|
|
AdapterType: gamedomain.AdapterYomiV4,
|
|
CallbackSecretCiphertext: secret,
|
|
CallbackIPWhitelist: []string{"3.1.174.194"},
|
|
AdapterConfigJSON: `{"uid_mode":"user_id"}`,
|
|
},
|
|
session: gamedomain.LaunchSession{
|
|
AppCode: "lalu",
|
|
SessionID: "game_sess_test",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
RoomID: "room_1",
|
|
PlatformCode: "yomi",
|
|
GameID: "yomi_fish_001",
|
|
ProviderGameID: "33",
|
|
LaunchTokenHash: stableHash(token),
|
|
Status: gamedomain.SessionActive,
|
|
ExpiresAtMS: 1700086400000,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 880}
|
|
svc := New(Config{}, repo, wallet, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
rawBody := encryptYomiTestBody(t, secret, `{"requestId":9001,"platUserId":"42","platToken":"gt_test_token","gold":120,"gameUid":33,"gameLevelUid":0,"reasonType":1,"gameRoundId":88,"timestamp":1700000000,"platPayload":"","platRoomId":"room_1"}`)
|
|
raw, contentType, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-yomi-change", AppCode: "lalu"},
|
|
PlatformCode: "yomi",
|
|
Operation: "change_balance",
|
|
RawBody: rawBody,
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback failed: %v", err)
|
|
}
|
|
if contentType != "application/json" || !strings.Contains(string(raw), `"code":200`) || !strings.Contains(string(raw), `"balance":880`) {
|
|
t.Fatalf("yomi change response mismatch: %s", raw)
|
|
}
|
|
if wallet.lastApply.GetCommandId() != "game:yomi:9001" || wallet.lastApply.GetOpType() != "debit" || wallet.lastApply.GetCoinAmount() != 120 {
|
|
t.Fatalf("wallet command mismatch: %+v", wallet.lastApply)
|
|
}
|
|
if repo.order.ProviderOrderID != "9001" || repo.order.ProviderRoundID != "88" || repo.order.GameID != "yomi_fish_001" {
|
|
t.Fatalf("game order mismatch: %+v", repo.order)
|
|
}
|
|
}
|
|
|
|
func TestHandleYomiRepairOrderOnlyLogs(t *testing.T) {
|
|
secret := "12345678901234567890123456789012"
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: "yomi",
|
|
AdapterType: gamedomain.AdapterYomiV4,
|
|
CallbackSecretCiphertext: secret,
|
|
AdapterConfigJSON: `{}`,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 880}
|
|
svc := New(Config{}, repo, wallet, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
rawBody := encryptYomiTestBody(t, secret, `{"requestId":9002,"platUserId":"42","gold":50,"gameUid":33,"gameRoundId":89,"rewardType":2,"winId":"","platRoomId":"room_1"}`)
|
|
raw, _, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-yomi-repair", AppCode: "lalu"},
|
|
PlatformCode: "yomi",
|
|
Operation: "repair_order",
|
|
RawBody: rawBody,
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"code":200`) {
|
|
t.Fatalf("repair response mismatch: %s", raw)
|
|
}
|
|
if repo.repair.ProviderOrderID != "9002" || repo.repair.Status != gamedomain.RepairStatusLogged {
|
|
t.Fatalf("repair order mismatch: %+v", repo.repair)
|
|
}
|
|
if wallet.lastApply != nil {
|
|
t.Fatalf("repair order must not apply wallet change: %+v", wallet.lastApply)
|
|
}
|
|
}
|
|
|
|
func TestHandleZeeOneUserInfoChangeBalanceAndUpdateSession(t *testing.T) {
|
|
secret := "zeeone-test-secret"
|
|
token := leaderccTestAccessToken(t, "lalu", 42, "420001", 1700003600)
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: "zeeone",
|
|
AdapterType: gamedomain.AdapterZeeOneV1,
|
|
CallbackSecretCiphertext: secret,
|
|
AdapterConfigJSON: `{"merchant_id":307715,"platform_id":2032,"uid_mode":"display_user_id"}`,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 1880}
|
|
user := &fakeUser{}
|
|
svc := New(Config{}, repo, wallet, user)
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
userBody := `{"appId":"M307715_P2032","userId":"420001","sessionId":"` + token + `","game_id":1021,"extra":"{\"vip\":1}"}`
|
|
raw, contentType, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-zeeone-user", AppCode: "lalu"},
|
|
PlatformCode: "zeeone",
|
|
Operation: "get_user_info",
|
|
RawBody: zeeoneTestEnvelope(t, secret, userBody, 1700000000000),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback userinfo failed: %v", err)
|
|
}
|
|
if contentType != "application/json" || !strings.Contains(string(raw), `"code":0`) || !strings.Contains(string(raw), `"balance":1880`) || !strings.Contains(string(raw), `"sessionId"`) {
|
|
t.Fatalf("zeeone userinfo response mismatch: %s", raw)
|
|
}
|
|
if user.lastGet.GetUserId() != 42 {
|
|
t.Fatalf("zeeone userinfo must query token user, got %+v", user.lastGet)
|
|
}
|
|
|
|
changeBody := `{"appId":"M307715_P2032","userId":"420001","sessionId":"` + token + `","currency_diff":-120,"diff_msg":"bet","game_id":1021,"room_id":"01","change_time_at":1700000000,"order_id":"zee_order_1","extend":"round_1"}`
|
|
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-zeeone-change", AppCode: "lalu"},
|
|
PlatformCode: "zeeone",
|
|
Operation: "change_balance",
|
|
RawBody: zeeoneTestEnvelope(t, secret, changeBody, 1700000000000),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback change failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"code":0`) || !strings.Contains(string(raw), `"currency_balance":1880`) {
|
|
t.Fatalf("zeeone change response mismatch: %s", raw)
|
|
}
|
|
if wallet.lastApply.GetUserId() != 42 || wallet.lastApply.GetOpType() != "debit" || wallet.lastApply.GetCoinAmount() != 120 || wallet.lastApply.GetGameId() != "1021" {
|
|
t.Fatalf("zeeone wallet command mismatch: %+v", wallet.lastApply)
|
|
}
|
|
|
|
updateBody := `{"appId":"M307715_P2032","userId":"420001","sessionId":"` + token + `","game_id":1021}`
|
|
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-zeeone-update-session", AppCode: "lalu"},
|
|
PlatformCode: "zeeone",
|
|
Operation: "update_session",
|
|
RawBody: zeeoneTestEnvelope(t, secret, updateBody, 1700000000000),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback update_session failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"code":0`) || !strings.Contains(string(raw), `"expireDate":1700003600000`) {
|
|
t.Fatalf("zeeone update_session response mismatch: %s", raw)
|
|
}
|
|
}
|
|
|
|
func TestHandleZeeOneAccessTokenIgnoresStaleLaunchSessionGameID(t *testing.T) {
|
|
secret := "zeeone-test-secret"
|
|
token := leaderccTestAccessToken(t, "lalu", 123456, "123456", 1700003600)
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: "zeeone",
|
|
AdapterType: gamedomain.AdapterZeeOneV1,
|
|
CallbackSecretCiphertext: secret,
|
|
AdapterConfigJSON: `{"merchant_id":307715,"platform_id":2032,"uid_mode":"display_user_id"}`,
|
|
},
|
|
// 同一个 App access token 启动过其他 ZeeOne 游戏时,旧启动会话会留下同 hash 但不同 provider_game_id。
|
|
// 回调必须以 JWT 用户身份为准,不能被旧会话的 game_id 拦截成 sessionId invalid。
|
|
session: gamedomain.LaunchSession{
|
|
AppCode: "lalu",
|
|
SessionID: "old_launch_session",
|
|
UserID: 123456,
|
|
DisplayUserID: "123456",
|
|
ProviderGameID: "1021",
|
|
LaunchTokenHash: stableHash(token),
|
|
Status: gamedomain.SessionActive,
|
|
ExpiresAtMS: 1700003600000,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 900}
|
|
svc := New(Config{}, repo, wallet, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
changeBody := `{"appId":"M307715_P2032","userId":"123456","sessionId":"` + token + `","currency_diff":-100,"diff_msg":"bet","game_id":1032,"room_id":"6","change_time_at":1700000000,"order_id":"zee_order_stale_session"}`
|
|
raw, _, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-zeeone-stale-session", AppCode: "lalu"},
|
|
PlatformCode: "zeeone",
|
|
Operation: "change_balance",
|
|
RawBody: zeeoneTestEnvelope(t, secret, changeBody, 1700000000000),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback change failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"code":0`) || strings.Contains(string(raw), "sessionId invalid") {
|
|
t.Fatalf("zeeone access token should ignore stale launch session game id: %s", raw)
|
|
}
|
|
if wallet.lastApply.GetUserId() != 123456 || wallet.lastApply.GetGameId() != "1032" {
|
|
t.Fatalf("zeeone wallet command must use JWT user and callback game id, got %+v", wallet.lastApply)
|
|
}
|
|
}
|
|
|
|
func TestHandleZeeOneAllowsDebitAndCreditWithSameProviderOrderID(t *testing.T) {
|
|
secret := "zeeone-test-secret"
|
|
token := leaderccTestAccessToken(t, "lalu", 123456, "123456", 1700003600)
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: "zeeone",
|
|
AdapterType: gamedomain.AdapterZeeOneV1,
|
|
CallbackSecretCiphertext: secret,
|
|
AdapterConfigJSON: `{"merchant_id":307715,"platform_id":2032,"uid_mode":"display_user_id"}`,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 900}
|
|
svc := New(Config{}, repo, wallet, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
base := `{"appId":"M307715_P2032","userId":"123456","sessionId":"` + token + `","diff_msg":"settle","game_id":1021,"room_id":"6","change_time_at":1700000000,"order_id":"zee_round_order_1"`
|
|
for _, item := range []struct {
|
|
name string
|
|
diff string
|
|
wantOp string
|
|
wantID string
|
|
wantCoins int64
|
|
}{
|
|
{name: "debit", diff: "-500", wantOp: "debit", wantID: "game:zeeone:zee_round_order_1:debit", wantCoins: 500},
|
|
{name: "credit", diff: "750", wantOp: "credit", wantID: "game:zeeone:zee_round_order_1:credit", wantCoins: 750},
|
|
} {
|
|
raw, _, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-zeeone-" + item.name, AppCode: "lalu"},
|
|
PlatformCode: "zeeone",
|
|
Operation: "change_balance",
|
|
RawBody: zeeoneTestEnvelope(t, secret, base+`,"currency_diff":`+item.diff+`}`, 1700000000000),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback %s failed: %v", item.name, err)
|
|
}
|
|
if !strings.Contains(string(raw), `"code":0`) {
|
|
t.Fatalf("zeeone %s response mismatch: %s", item.name, raw)
|
|
}
|
|
if wallet.lastApply.GetCommandId() != item.wantID || wallet.lastApply.GetOpType() != item.wantOp || wallet.lastApply.GetCoinAmount() != item.wantCoins {
|
|
t.Fatalf("zeeone %s wallet command mismatch: %+v", item.name, wallet.lastApply)
|
|
}
|
|
}
|
|
if wallet.applyCount != 2 {
|
|
t.Fatalf("debit and credit should both reach wallet, got %d applies", wallet.applyCount)
|
|
}
|
|
}
|
|
|
|
func TestHandleBaishunSSTokenUserInfoUpdateAndChangeBalance(t *testing.T) {
|
|
secret := "baishun-test-app-key"
|
|
token := "app_access_token_baishun"
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: "baishun",
|
|
AdapterType: gamedomain.AdapterBaishunV1,
|
|
CallbackSecretCiphertext: secret,
|
|
AdapterConfigJSON: `{"app_id":21397507,"app_channel":"mesh","uid_mode":"display_user_id"}`,
|
|
},
|
|
session: gamedomain.LaunchSession{
|
|
AppCode: "lalu",
|
|
SessionID: "game_sess_baishun",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
RoomID: "room_1",
|
|
PlatformCode: "baishun",
|
|
GameID: "baishun_greedy",
|
|
ProviderGameID: "1006",
|
|
LaunchTokenHash: stableHash(token),
|
|
Status: gamedomain.SessionActive,
|
|
ExpiresAtMS: 1700086400000,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 1880}
|
|
user := &fakeUser{}
|
|
svc := New(Config{}, repo, wallet, user)
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
raw, contentType, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-baishun-sstoken", AppCode: "lalu"},
|
|
PlatformCode: "baishun",
|
|
Operation: "get_sstoken",
|
|
RawBody: baishunTestBody(t, secret, `{"app_id":21397507,"user_id":"420001","code":"app_access_token_baishun"}`, 1700000000),
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback get_sstoken failed: %v", err)
|
|
}
|
|
if contentType != "application/json" || !strings.Contains(string(raw), `"code":0`) || !strings.Contains(string(raw), `"ss_token":"app_access_token_baishun"`) || !strings.Contains(string(raw), `"user_info"`) || !strings.Contains(string(raw), `"balance":1880`) {
|
|
t.Fatalf("baishun get_sstoken response mismatch: %s", raw)
|
|
}
|
|
if user.lastGet.GetUserId() != 42 {
|
|
t.Fatalf("baishun get_sstoken must query token user, got %+v", user.lastGet)
|
|
}
|
|
|
|
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-baishun-userinfo", AppCode: "lalu"},
|
|
PlatformCode: "baishun",
|
|
Operation: "get_user_info",
|
|
RawBody: baishunTestBody(t, secret, `{"app_id":21397507,"user_id":"420001","ss_token":"app_access_token_baishun","client_ip":"110.86.1.130","game_id":1006}`, 1700000000),
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback get_user_info failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"code":0`) || !strings.Contains(string(raw), `"user_id":"420001"`) || !strings.Contains(string(raw), `"user_avatar":"https://cdn.example/avatar.png"`) {
|
|
t.Fatalf("baishun userinfo response mismatch: %s", raw)
|
|
}
|
|
|
|
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-baishun-update-sstoken", AppCode: "lalu"},
|
|
PlatformCode: "baishun",
|
|
Operation: "update_sstoken",
|
|
RawBody: baishunTestBody(t, secret, `{"app_id":21397507,"user_id":"420001","ss_token":"app_access_token_baishun","game_id":1006}`, 1700000000),
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback update_sstoken failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"code":0`) || !strings.Contains(string(raw), `"expire_date":1700086400000`) {
|
|
t.Fatalf("baishun update_sstoken response mismatch: %s", raw)
|
|
}
|
|
|
|
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-baishun-change", AppCode: "lalu"},
|
|
PlatformCode: "baishun",
|
|
Operation: "change_balance",
|
|
RawBody: baishunTestBody(t, secret, `{"app_id":21397507,"user_id":"420001","ss_token":"app_access_token_baishun","currency_diff":-120,"diff_msg":"bet","game_id":1006,"room_id":"room_1","game_round_id":"round_1","change_time_at":1700000000,"order_id":"bs_order_1","extend":"{\"source\":\"test\"}"}`, 1700000000),
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback change_balance failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"code":0`) || !strings.Contains(string(raw), `"currency_balance":1880`) || !strings.Contains(string(raw), `"extend":"{\"source\":\"test\"}"`) {
|
|
t.Fatalf("baishun change response mismatch: %s", raw)
|
|
}
|
|
if wallet.lastApply.GetCommandId() != "game:baishun:bs_order_1" || wallet.lastApply.GetOpType() != "debit" || wallet.lastApply.GetCoinAmount() != 120 || wallet.lastApply.GetGameId() != "baishun_greedy" {
|
|
t.Fatalf("baishun wallet command mismatch: %+v", wallet.lastApply)
|
|
}
|
|
}
|
|
|
|
func TestHandleVivaGamesTokenUserInfoUpdateAndChangeBalance(t *testing.T) {
|
|
secret := "vivagames-test-app-key"
|
|
token := "app_access_token_viva"
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: "vivagames",
|
|
AdapterType: gamedomain.AdapterVivaGamesV1,
|
|
CallbackSecretCiphertext: secret,
|
|
AdapterConfigJSON: `{"app_id":1012127974,"uid_mode":"display_user_id"}`,
|
|
},
|
|
session: gamedomain.LaunchSession{
|
|
AppCode: "lalu",
|
|
SessionID: "game_sess_viva",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
RoomID: "room_1",
|
|
PlatformCode: "vivagames",
|
|
GameID: "vivagames_2",
|
|
ProviderGameID: "2",
|
|
LaunchTokenHash: stableHash(token),
|
|
Status: gamedomain.SessionActive,
|
|
ExpiresAtMS: 1700086400000,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 1880}
|
|
user := &fakeUser{}
|
|
svc := New(Config{}, repo, wallet, user)
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
raw, contentType, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-viva-token", AppCode: "lalu"},
|
|
PlatformCode: "vivagames",
|
|
Operation: "get-token",
|
|
RawBody: vivaGamesTestBody(t, secret, `{"app_id":1012127974,"user_id":"420001","code":"app_access_token_viva"}`, 1700000000),
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback get-token failed: %v", err)
|
|
}
|
|
if contentType != "application/json" || !strings.Contains(string(raw), `"code":0`) || !strings.Contains(string(raw), `"token":"app_access_token_viva"`) || !strings.Contains(string(raw), `"expire_at":1700086400000`) {
|
|
t.Fatalf("vivagames get-token response mismatch: %s", raw)
|
|
}
|
|
|
|
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-viva-userinfo", AppCode: "lalu"},
|
|
PlatformCode: "vivagames",
|
|
Operation: "get-user-info",
|
|
RawBody: vivaGamesTestBody(t, secret, `{"app_id":1012127974,"user_id":"420001","token":"app_access_token_viva","client_ip":"110.86.1.130","game_id":2}`, 1700000000),
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback get-user-info failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"code":0`) || !strings.Contains(string(raw), `"user_id":"420001"`) || !strings.Contains(string(raw), `"avatar_url":"https://cdn.example/avatar.png"`) || !strings.Contains(string(raw), `"balance":1880`) {
|
|
t.Fatalf("vivagames userinfo response mismatch: %s", raw)
|
|
}
|
|
|
|
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-viva-update", AppCode: "lalu"},
|
|
PlatformCode: "vivagames",
|
|
Operation: "update-token",
|
|
RawBody: vivaGamesTestBody(t, secret, `{"app_id":1012127974,"user_id":"420001","token":"app_access_token_viva"}`, 1700000000),
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback update-token failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"code":0`) || !strings.Contains(string(raw), `"expire_at":1700086400000`) {
|
|
t.Fatalf("vivagames update-token response mismatch: %s", raw)
|
|
}
|
|
|
|
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-viva-change", AppCode: "lalu"},
|
|
PlatformCode: "vivagames",
|
|
Operation: "change-balance",
|
|
RawBody: vivaGamesTestBody(t, secret, `{"app_id":1012127974,"user_id":"420001","token":"app_access_token_viva","order_id":"viva_order_1","game_round_id":"round_1","game_id":2,"total_bet":120,"change_amount":-120,"change_reason":"bet","change_time_at":1700000000,"metadata":"{\"room_id\":\"room_1\"}"}`, 1700000000),
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback change-balance failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"code":0`) || !strings.Contains(string(raw), `"balance":1880`) {
|
|
t.Fatalf("vivagames change response mismatch: %s", raw)
|
|
}
|
|
if wallet.lastApply.GetCommandId() != "game:vivagames:viva_order_1" || wallet.lastApply.GetOpType() != "debit" || wallet.lastApply.GetCoinAmount() != 120 || wallet.lastApply.GetGameId() != "vivagames_2" {
|
|
t.Fatalf("vivagames wallet command mismatch: %+v", wallet.lastApply)
|
|
}
|
|
}
|
|
|
|
func TestHandleReyouUserInfoAndUpdateBalance(t *testing.T) {
|
|
secret := "reyou-test-key"
|
|
token := "app_access_token_reyou"
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: "reyou",
|
|
AdapterType: gamedomain.AdapterReyouV1,
|
|
CallbackSecretCiphertext: secret,
|
|
AdapterConfigJSON: `{"uid_mode":"display_user_id"}`,
|
|
},
|
|
session: gamedomain.LaunchSession{
|
|
AppCode: "lalu",
|
|
SessionID: "game_sess_reyou",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
RoomID: "room_1",
|
|
PlatformCode: "reyou",
|
|
GameID: "reyou_101",
|
|
ProviderGameID: "101",
|
|
LaunchTokenHash: stableHash(token),
|
|
Status: gamedomain.SessionActive,
|
|
ExpiresAtMS: 1700086400000,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 1880}
|
|
user := &fakeUser{}
|
|
svc := New(Config{}, repo, wallet, user)
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
userInfoSign := reyouTestSign(secret, "101", "420001", token)
|
|
raw, contentType, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-reyou-userinfo", AppCode: "lalu"},
|
|
PlatformCode: "reyou",
|
|
Operation: "getUserInfo",
|
|
RawBody: []byte(`{"gameId":"101","uid":"420001","token":"` + token + `","sign":"` + userInfoSign + `"}`),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback getUserInfo failed: %v", err)
|
|
}
|
|
if contentType != "application/json" || !strings.Contains(string(raw), `"errorCode":0`) || !strings.Contains(string(raw), `"uid":"420001"`) || !strings.Contains(string(raw), `"coin":1880`) || !strings.Contains(string(raw), `"vipLevel":0`) {
|
|
t.Fatalf("reyou userinfo response mismatch: %s", raw)
|
|
}
|
|
if user.lastGet.GetUserId() != 42 {
|
|
t.Fatalf("user request mismatch: %+v", user.lastGet)
|
|
}
|
|
|
|
changeSign := reyouTestSign(secret, "reyou_order_1", "101", "round_1", "420001", "120", "1", token)
|
|
changeBody := `{"orderId":"reyou_order_1","gameId":"101","roundId":"round_1","uid":"420001","coin":120,"type":1,"token":"` + token + `","sign":"` + changeSign + `"}`
|
|
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-reyou-change", AppCode: "lalu"},
|
|
PlatformCode: "reyou",
|
|
Operation: "updateBalance",
|
|
RawBody: []byte(changeBody),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback updateBalance failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"errorCode":0`) || !strings.Contains(string(raw), `"coin":1880`) || !strings.Contains(string(raw), `"responseId":"reyou_`) {
|
|
t.Fatalf("reyou change response mismatch: %s", raw)
|
|
}
|
|
if wallet.lastApply.GetCommandId() != "game:reyou:reyou_order_1" || wallet.lastApply.GetOpType() != "debit" || wallet.lastApply.GetCoinAmount() != 120 || wallet.lastApply.GetGameId() != "reyou_101" {
|
|
t.Fatalf("reyou wallet command mismatch: %+v", wallet.lastApply)
|
|
}
|
|
if wallet.applyCount != 1 {
|
|
t.Fatalf("wallet apply count = %d, want 1", wallet.applyCount)
|
|
}
|
|
|
|
raw, _, err = svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-reyou-change-replay", AppCode: "lalu"},
|
|
PlatformCode: "reyou",
|
|
Operation: "updateBalance",
|
|
RawBody: []byte(changeBody),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("duplicate updateBalance failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"errorCode":3001`) || !strings.Contains(string(raw), `"coin":1880`) {
|
|
t.Fatalf("reyou duplicate response mismatch: %s", raw)
|
|
}
|
|
if wallet.applyCount != 1 {
|
|
t.Fatalf("duplicate order must not apply wallet again, count=%d", wallet.applyCount)
|
|
}
|
|
}
|
|
|
|
func TestHandleBaishunAccessTokenIgnoresStaleLaunchSessionGameID(t *testing.T) {
|
|
secret := "baishun-test-app-key"
|
|
userID := int64(312900923573673984)
|
|
token := leaderccTestAccessToken(t, "lalu", userID, "163004", 1700003600)
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: "baishun",
|
|
AdapterType: gamedomain.AdapterBaishunV1,
|
|
CallbackSecretCiphertext: secret,
|
|
AdapterConfigJSON: `{"app_id":7482496867,"app_channel":"bobi","uid_mode":"display_user_id"}`,
|
|
},
|
|
// 同一个 App access token 会被百顺 H5 作为 ss_token 复用;旧启动会话只说明曾经打开过别的游戏。
|
|
// 回调验权必须以 JWT 里的当前用户身份为准,否则 user_info 会被旧 provider_game_id 拦成 token invalid。
|
|
session: gamedomain.LaunchSession{
|
|
AppCode: "lalu",
|
|
SessionID: "old_baishun_launch_session",
|
|
UserID: userID,
|
|
DisplayUserID: "163004",
|
|
PlatformCode: "baishun",
|
|
ProviderGameID: "1006",
|
|
LaunchTokenHash: stableHash(token),
|
|
Status: gamedomain.SessionActive,
|
|
ExpiresAtMS: 1700086400000,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 1880}
|
|
user := &fakeUser{}
|
|
svc := New(Config{}, repo, wallet, user)
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
raw, _, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-baishun-stale-session", AppCode: "lalu"},
|
|
PlatformCode: "baishun",
|
|
Operation: "get_user_info",
|
|
RawBody: baishunTestBody(t, secret, `{"app_id":7482496867,"user_id":"163004","ss_token":"`+token+`","provider_name":"bobi","client_ip":"104.28.244.150","game_id":1043,"currency_type":0}`, 1700000000),
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback get_user_info failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"code":0`) || strings.Contains(string(raw), "token invalid") || !strings.Contains(string(raw), `"user_id":"163004"`) {
|
|
t.Fatalf("baishun access token should ignore stale launch session game id: %s", raw)
|
|
}
|
|
if user.lastGet.GetUserId() != userID {
|
|
t.Fatalf("baishun userinfo must query JWT user, got %+v", user.lastGet)
|
|
}
|
|
}
|
|
|
|
func TestHandleBaishunGetSSTokenReturnsTokenInvalidWhenUserMissing(t *testing.T) {
|
|
secret := "baishun-test-app-key"
|
|
token := "app_access_token_baishun"
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: "baishun",
|
|
AdapterType: gamedomain.AdapterBaishunV1,
|
|
CallbackSecretCiphertext: secret,
|
|
AdapterConfigJSON: `{"app_id":21397507,"app_channel":"mesh","uid_mode":"display_user_id"}`,
|
|
},
|
|
session: gamedomain.LaunchSession{
|
|
AppCode: "lalu",
|
|
SessionID: "game_sess_baishun_missing_user",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
RoomID: "room_1",
|
|
PlatformCode: "baishun",
|
|
GameID: "baishun_greedy",
|
|
ProviderGameID: "1006",
|
|
LaunchTokenHash: stableHash(token),
|
|
Status: gamedomain.SessionActive,
|
|
ExpiresAtMS: 1700086400000,
|
|
},
|
|
}
|
|
svc := New(Config{}, repo, &fakeWallet{}, &fakeUser{err: xerr.New(xerr.NotFound, "user not found")})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
raw, contentType, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-baishun-missing-user", AppCode: "lalu"},
|
|
PlatformCode: "baishun",
|
|
Operation: "get_sstoken",
|
|
RawBody: baishunTestBody(t, secret, `{"app_id":21397507,"user_id":"420001","code":"app_access_token_baishun"}`, 1700000000),
|
|
RemoteAddr: "3.1.174.194:443",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback get_sstoken failed: %v", err)
|
|
}
|
|
if contentType != "application/json" || !strings.Contains(string(raw), `"code":1001`) || !strings.Contains(string(raw), `"message":"token invalid"`) {
|
|
t.Fatalf("baishun missing user response mismatch: %s", raw)
|
|
}
|
|
if strings.Contains(string(raw), `"code":1002`) || strings.Contains(string(raw), "read data failed") {
|
|
t.Fatalf("baishun missing user must not look like read data failure: %s", raw)
|
|
}
|
|
}
|
|
|
|
func TestHandleCallbackChangeCoinUsesWalletAndOrderIdempotency(t *testing.T) {
|
|
repo := &fakeRepository{
|
|
launchable: gamedomain.LaunchableGame{CatalogItem: gamedomain.CatalogItem{GameID: "demo_rocket_001", ProviderGameID: "rocket_001"}},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 880}
|
|
svc := New(Config{}, repo, wallet, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
req := &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-callback", AppCode: "lalu"},
|
|
PlatformCode: "demo",
|
|
Operation: "change_coin",
|
|
RawBody: []byte(`{"user_id":42,"game_id":"demo_rocket_001","provider_order_id":"porder_1","op_type":"debit","coin_amount":120,"room_id":"room_1"}`),
|
|
}
|
|
raw, contentType, err := svc.HandleCallback(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback failed: %v", err)
|
|
}
|
|
if contentType != "application/json" || !strings.Contains(string(raw), `"balance_after":880`) {
|
|
t.Fatalf("callback response mismatch: contentType=%s raw=%s", contentType, raw)
|
|
}
|
|
if wallet.lastApply.GetCommandId() != "game:demo:porder_1" || wallet.lastApply.GetCoinAmount() != 120 {
|
|
t.Fatalf("wallet command mismatch: %+v", wallet.lastApply)
|
|
}
|
|
if repo.order.Status != gamedomain.OrderStatusSucceeded || repo.order.WalletTransactionID != "wtx-game-1" {
|
|
t.Fatalf("order state mismatch: %+v", repo.order)
|
|
}
|
|
if len(repo.levelEvents) != 1 || repo.levelEvents[0].CoinAmount != 120 {
|
|
t.Fatalf("level outbox event mismatch: %+v", repo.levelEvents)
|
|
}
|
|
|
|
raw, _, err = svc.HandleCallback(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("idempotent callback failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"idempotent_replay":true`) {
|
|
t.Fatalf("idempotent response mismatch: %s", raw)
|
|
}
|
|
if len(repo.levelEvents) != 1 {
|
|
t.Fatalf("idempotent replay should not duplicate level outbox events: %+v", repo.levelEvents)
|
|
}
|
|
}
|
|
|
|
func TestHandleCallbackGetUserInfoUsesUserAndWallet(t *testing.T) {
|
|
wallet := &fakeWallet{balanceAfter: 1680}
|
|
user := &fakeUser{}
|
|
svc := New(Config{}, &fakeRepository{}, wallet, user)
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
raw, contentType, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{RequestId: "req-user-info", AppCode: "lalu"},
|
|
PlatformCode: "demo",
|
|
Operation: "get_user_info",
|
|
RawBody: []byte(`{"user_id":42}`),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback failed: %v", err)
|
|
}
|
|
if contentType != "application/json" || !strings.Contains(string(raw), `"display_user_id":"420001"`) || !strings.Contains(string(raw), `"balance":1680`) {
|
|
t.Fatalf("user info response mismatch: %s", raw)
|
|
}
|
|
if user.lastGet.GetUserId() != 42 {
|
|
t.Fatalf("user request mismatch: %+v", user.lastGet)
|
|
}
|
|
}
|
|
|
|
func TestProcessLevelEventOutboxBatchRelaysGameSpend(t *testing.T) {
|
|
repo := &fakeRepository{levelEvents: []gamedomain.LevelEventOutbox{{
|
|
AppCode: "lalu",
|
|
EventID: "game_level:order_1",
|
|
OrderID: "order_1",
|
|
UserID: 42,
|
|
GameID: "demo_rocket_001",
|
|
ProviderOrderID: "porder_1",
|
|
ProviderRoundID: "round_1",
|
|
CoinAmount: 120,
|
|
WalletTransactionID: "wtx-game-1",
|
|
PayloadJSON: `{"game_id":"demo_rocket_001"}`,
|
|
OccurredAtMS: 1700000000000,
|
|
}}}
|
|
activity := &fakeActivity{status: "consumed"}
|
|
svc := New(Config{}, repo, &fakeWallet{}, &fakeUser{}, activity)
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000001000) }
|
|
|
|
claimed, processed, success, failure, hasMore, err := svc.ProcessLevelEventOutboxBatch(context.Background(), "run_1", "worker_1", 100, 30*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("ProcessLevelEventOutboxBatch failed: %v", err)
|
|
}
|
|
if claimed != 1 || processed != 1 || success != 1 || failure != 0 || hasMore {
|
|
t.Fatalf("batch result mismatch: claimed=%d processed=%d success=%d failure=%d hasMore=%v", claimed, processed, success, failure, hasMore)
|
|
}
|
|
if activity.last.GetTrack() != "game" || activity.last.GetMetricType() != "game_spend_coin" || activity.last.GetValueDelta() != 120 {
|
|
t.Fatalf("activity relay request mismatch: %+v", activity.last)
|
|
}
|
|
if len(repo.deliveredEvents) != 1 || repo.deliveredEvents[0] != "game_level:order_1" {
|
|
t.Fatalf("delivered state mismatch: %+v", repo.deliveredEvents)
|
|
}
|
|
}
|
|
|
|
type fakeRepository struct {
|
|
launchable gamedomain.LaunchableGame
|
|
platform gamedomain.Platform
|
|
session gamedomain.LaunchSession
|
|
order gamedomain.GameOrder
|
|
orders []gamedomain.GameOrder
|
|
repair gamedomain.RepairOrder
|
|
levelEvents []gamedomain.LevelEventOutbox
|
|
recentGames []gamedomain.AppGame
|
|
lastRecentQuery ListRecentGamesQuery
|
|
platforms []gamedomain.Platform
|
|
lastListPlatformsAppCode string
|
|
lastListPlatformsStatus string
|
|
deliveredEvents []string
|
|
failedEvents []string
|
|
}
|
|
|
|
func (f *fakeRepository) Ping(context.Context) error { return nil }
|
|
func (f *fakeRepository) ListGames(context.Context, ListGamesQuery) ([]gamedomain.AppGame, error) {
|
|
return []gamedomain.AppGame{{GameID: "demo_rocket_001"}}, nil
|
|
}
|
|
func (f *fakeRepository) ListRecentGames(_ context.Context, query ListRecentGamesQuery) ([]gamedomain.AppGame, error) {
|
|
f.lastRecentQuery = query
|
|
if f.recentGames != nil {
|
|
return f.recentGames, nil
|
|
}
|
|
return []gamedomain.AppGame{{GameID: "demo_recent_001"}}, nil
|
|
}
|
|
func (f *fakeRepository) GetLaunchableGame(context.Context, string, string) (gamedomain.LaunchableGame, error) {
|
|
return f.launchable, nil
|
|
}
|
|
func (f *fakeRepository) GetPlatform(context.Context, string, string) (gamedomain.Platform, error) {
|
|
return f.platform, nil
|
|
}
|
|
func (f *fakeRepository) CreateLaunchSession(_ context.Context, session gamedomain.LaunchSession) error {
|
|
f.session = session
|
|
return nil
|
|
}
|
|
func (f *fakeRepository) GetLaunchSessionByToken(_ context.Context, _ string, token string) (gamedomain.LaunchSession, error) {
|
|
if f.session.LaunchTokenHash == stableHash(token) {
|
|
return f.session, nil
|
|
}
|
|
return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "session token invalid")
|
|
}
|
|
func (f *fakeRepository) InsertCallbackLog(context.Context, gamedomain.CallbackLog) error { return nil }
|
|
func (f *fakeRepository) CreateGameOrder(_ context.Context, order gamedomain.GameOrder) (gamedomain.GameOrder, bool, error) {
|
|
for _, existing := range f.orders {
|
|
if existing.OrderID == order.OrderID {
|
|
f.order = existing
|
|
return existing, true, nil
|
|
}
|
|
}
|
|
if f.order.OrderID != "" && len(f.orders) == 0 {
|
|
if f.order.OrderID == order.OrderID {
|
|
return f.order, true, nil
|
|
}
|
|
f.orders = append(f.orders, f.order)
|
|
}
|
|
f.order = order
|
|
f.orders = append(f.orders, order)
|
|
return order, false, nil
|
|
}
|
|
func (f *fakeRepository) MarkOrderSucceeded(_ context.Context, _ string, orderID string, walletTransactionID string, balanceAfter int64, nowMs int64) error {
|
|
f.order.OrderID = orderID
|
|
f.order.Status = gamedomain.OrderStatusSucceeded
|
|
f.order.WalletTransactionID = walletTransactionID
|
|
f.order.WalletBalanceAfter = balanceAfter
|
|
f.order.UpdatedAtMS = nowMs
|
|
for index := range f.orders {
|
|
if f.orders[index].OrderID == orderID {
|
|
f.orders[index] = f.order
|
|
break
|
|
}
|
|
}
|
|
if strings.EqualFold(f.order.OpType, "debit") && f.order.CoinAmount > 0 && len(f.levelEvents) == 0 {
|
|
f.levelEvents = append(f.levelEvents, gamedomain.LevelEventOutbox{
|
|
AppCode: f.order.AppCode,
|
|
EventID: "game_level:" + f.order.OrderID,
|
|
OrderID: f.order.OrderID,
|
|
UserID: f.order.UserID,
|
|
GameID: f.order.GameID,
|
|
ProviderOrderID: f.order.ProviderOrderID,
|
|
ProviderRoundID: f.order.ProviderRoundID,
|
|
CoinAmount: f.order.CoinAmount,
|
|
WalletTransactionID: walletTransactionID,
|
|
OccurredAtMS: nowMs,
|
|
CreatedAtMS: nowMs,
|
|
UpdatedAtMS: nowMs,
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
func (f *fakeRepository) MarkOrderFailed(_ context.Context, _ string, orderID string, status string, code string, message string, nowMs int64) error {
|
|
f.order.OrderID = orderID
|
|
f.order.Status = status
|
|
f.order.FailureCode = code
|
|
f.order.FailureMessage = message
|
|
f.order.UpdatedAtMS = nowMs
|
|
for index := range f.orders {
|
|
if f.orders[index].OrderID == orderID {
|
|
f.orders[index] = f.order
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
func (f *fakeRepository) CreateRepairOrder(_ context.Context, order gamedomain.RepairOrder) (gamedomain.RepairOrder, bool, error) {
|
|
if f.repair.RepairID != "" {
|
|
return f.repair, true, nil
|
|
}
|
|
f.repair = order
|
|
return order, false, nil
|
|
}
|
|
func (f *fakeRepository) ClaimPendingLevelEvents(context.Context, string, int64, time.Duration, int) ([]gamedomain.LevelEventOutbox, error) {
|
|
events := append([]gamedomain.LevelEventOutbox(nil), f.levelEvents...)
|
|
f.levelEvents = nil
|
|
return events, nil
|
|
}
|
|
func (f *fakeRepository) MarkLevelEventDelivered(_ context.Context, eventID string, _ int64) error {
|
|
f.deliveredEvents = append(f.deliveredEvents, eventID)
|
|
return nil
|
|
}
|
|
func (f *fakeRepository) MarkLevelEventFailed(_ context.Context, eventID string, _ string, _ int64, _ int64) error {
|
|
f.failedEvents = append(f.failedEvents, eventID)
|
|
return nil
|
|
}
|
|
func (f *fakeRepository) ListPlatforms(_ context.Context, appCode string, status string) ([]gamedomain.Platform, error) {
|
|
f.lastListPlatformsAppCode = appCode
|
|
f.lastListPlatformsStatus = status
|
|
if f.platforms != nil {
|
|
return f.platforms, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
func (f *fakeRepository) UpsertPlatform(context.Context, gamedomain.Platform) (gamedomain.Platform, error) {
|
|
return gamedomain.Platform{}, nil
|
|
}
|
|
func (f *fakeRepository) ListCatalog(context.Context, ListCatalogQuery) ([]gamedomain.CatalogItem, string, error) {
|
|
return nil, "", nil
|
|
}
|
|
func (f *fakeRepository) UpsertCatalog(context.Context, gamedomain.CatalogItem) (gamedomain.CatalogItem, error) {
|
|
return gamedomain.CatalogItem{}, nil
|
|
}
|
|
func (f *fakeRepository) SetGameStatus(context.Context, string, string, string, int64) (gamedomain.CatalogItem, error) {
|
|
return gamedomain.CatalogItem{}, nil
|
|
}
|
|
func (f *fakeRepository) DeleteCatalog(context.Context, string, string) error {
|
|
return nil
|
|
}
|
|
|
|
type fakeWallet struct {
|
|
lastApply *walletv1.ApplyGameCoinChangeRequest
|
|
applyCount int
|
|
balanceAfter int64
|
|
}
|
|
|
|
func (f *fakeWallet) ApplyGameCoinChange(_ context.Context, req *walletv1.ApplyGameCoinChangeRequest) (*walletv1.ApplyGameCoinChangeResponse, error) {
|
|
f.lastApply = req
|
|
f.applyCount++
|
|
return &walletv1.ApplyGameCoinChangeResponse{WalletTransactionId: "wtx-game-1", BalanceAfter: f.balanceAfter}, nil
|
|
}
|
|
|
|
func (f *fakeWallet) GetBalances(context.Context, *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) {
|
|
return &walletv1.GetBalancesResponse{Balances: []*walletv1.AssetBalance{{AssetType: "COIN", AvailableAmount: f.balanceAfter}}}, nil
|
|
}
|
|
|
|
type fakeActivity struct {
|
|
status string
|
|
last *activityv1.ConsumeLevelEventRequest
|
|
}
|
|
|
|
func (f *fakeActivity) ConsumeLevelEvent(_ context.Context, req *activityv1.ConsumeLevelEventRequest, _ ...grpc.CallOption) (*activityv1.ConsumeLevelEventResponse, error) {
|
|
f.last = req
|
|
status := f.status
|
|
if status == "" {
|
|
status = "consumed"
|
|
}
|
|
return &activityv1.ConsumeLevelEventResponse{EventId: req.GetEventId(), Status: status}, nil
|
|
}
|
|
|
|
type fakeUser struct {
|
|
lastGet *userv1.GetUserRequest
|
|
err error
|
|
}
|
|
|
|
func (f *fakeUser) GetUser(_ context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
|
|
f.lastGet = req
|
|
if f.err != nil {
|
|
return nil, f.err
|
|
}
|
|
return &userv1.GetUserResponse{User: &userv1.User{
|
|
UserId: req.GetUserId(),
|
|
DisplayUserId: "420001",
|
|
Username: "tester",
|
|
Avatar: "https://cdn.example/avatar.png",
|
|
Country: "AE",
|
|
RegionId: 100,
|
|
}}, nil
|
|
}
|
|
|
|
func encryptYomiTestBody(t *testing.T, secret string, plain string) []byte {
|
|
t.Helper()
|
|
block, err := aes.NewCipher([]byte(secret))
|
|
if err != nil {
|
|
t.Fatalf("aes cipher failed: %v", err)
|
|
}
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
t.Fatalf("gcm failed: %v", err)
|
|
}
|
|
nonce := []byte("123456789012")
|
|
payload := append([]byte{}, nonce...)
|
|
payload = append(payload, gcm.Seal(nil, nonce, []byte(plain), nil)...)
|
|
return []byte(base64.URLEncoding.EncodeToString(payload))
|
|
}
|
|
|
|
func leaderccTestSign(key string, parts ...string) string {
|
|
builder := strings.Builder{}
|
|
for _, part := range parts {
|
|
builder.WriteString(strings.TrimSpace(part))
|
|
}
|
|
builder.WriteString(strings.TrimSpace(key))
|
|
sum := md5.Sum([]byte(builder.String()))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func reyouTestSign(key string, parts ...string) string {
|
|
builder := strings.Builder{}
|
|
for _, part := range parts {
|
|
builder.WriteString(strings.TrimSpace(part))
|
|
}
|
|
builder.WriteString(strings.TrimSpace(key))
|
|
sum := md5.Sum([]byte(builder.String()))
|
|
return strings.ToUpper(hex.EncodeToString(sum[:]))
|
|
}
|
|
|
|
func leaderccTestAccessToken(t *testing.T, app string, userID int64, displayUserID string, expiresAtSec int64) string {
|
|
t.Helper()
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
"iss": "hyapp",
|
|
"app_code": app,
|
|
"sub": strconv.FormatInt(userID, 10),
|
|
"user_id": strconv.FormatInt(userID, 10),
|
|
"display_user_id": displayUserID,
|
|
"default_display_user_id": displayUserID,
|
|
"sid": "sess_test_app_token",
|
|
"typ": "access",
|
|
"iat": int64(1700000000),
|
|
"exp": expiresAtSec,
|
|
})
|
|
signed, err := token.SignedString([]byte("test-secret"))
|
|
if err != nil {
|
|
t.Fatalf("sign test access token: %v", err)
|
|
}
|
|
return signed
|
|
}
|
|
|
|
func zeeoneTestEnvelope(t *testing.T, secret string, body string, timestamp int64) []byte {
|
|
t.Helper()
|
|
nonce := "5f0eb04d7603a9d8"
|
|
raw := nonce + body + strconv.FormatInt(timestamp, 10) + secret
|
|
sum := md5.Sum([]byte(raw))
|
|
signature := hex.EncodeToString(sum[:])
|
|
encodedBody, err := json.Marshal(body)
|
|
if err != nil {
|
|
t.Fatalf("marshal reqbody: %v", err)
|
|
}
|
|
return []byte(`{"reqbody":` + string(encodedBody) + `,"signature":"` + signature + `","signature_nonce":"` + nonce + `","timestamp":` + strconv.FormatInt(timestamp, 10) + `}`)
|
|
}
|
|
|
|
func baishunTestBody(t *testing.T, secret string, body string, timestamp int64) []byte {
|
|
t.Helper()
|
|
nonceSum := md5.Sum([]byte(body))
|
|
nonce := hex.EncodeToString(nonceSum[:8])
|
|
raw := nonce + strings.TrimSpace(secret) + strconv.FormatInt(timestamp, 10)
|
|
sum := md5.Sum([]byte(raw))
|
|
var payload map[string]any
|
|
decoder := json.NewDecoder(strings.NewReader(body))
|
|
decoder.UseNumber()
|
|
if err := decoder.Decode(&payload); err != nil {
|
|
t.Fatalf("decode baishun test body: %v", err)
|
|
}
|
|
payload["signature_nonce"] = nonce
|
|
payload["timestamp"] = timestamp
|
|
payload["signature"] = hex.EncodeToString(sum[:])
|
|
encoded, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("marshal baishun test body: %v", err)
|
|
}
|
|
return encoded
|
|
}
|
|
|
|
func vivaGamesTestBody(t *testing.T, secret string, body string, timestamp int64) []byte {
|
|
t.Helper()
|
|
var payload map[string]any
|
|
decoder := json.NewDecoder(strings.NewReader(body))
|
|
decoder.UseNumber()
|
|
if err := decoder.Decode(&payload); err != nil {
|
|
t.Fatalf("decode vivagames test body: %v", err)
|
|
}
|
|
payload["timestamp"] = json.Number(strconv.FormatInt(timestamp, 10))
|
|
payload["sign"] = vivaGamesSign(payload, secret)
|
|
encoded, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("marshal vivagames test body: %v", err)
|
|
}
|
|
return encoded
|
|
}
|