233 lines
7.8 KiB
Go
233 lines
7.8 KiB
Go
package game
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/url"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
gamev1 "hyapp.local/api/proto/game/v1"
|
|
gamedomain "hyapp/services/game-service/internal/domain/game"
|
|
)
|
|
|
|
func TestLaunchGameBuildsAMGURL(t *testing.T) {
|
|
repo := &fakeRepository{
|
|
launchable: gamedomain.LaunchableGame{
|
|
CatalogItem: gamedomain.CatalogItem{
|
|
AppCode: "lalu",
|
|
GameID: "amg_1001",
|
|
PlatformCode: gamedomain.PlatformCodeAMG,
|
|
ProviderGameID: "1001",
|
|
GameName: "AMG Test",
|
|
Status: gamedomain.StatusActive,
|
|
Orientation: "portrait",
|
|
},
|
|
PlatformStatus: gamedomain.StatusActive,
|
|
AdapterType: gamedomain.AdapterAMGV1,
|
|
AdapterConfigJSON: `{
|
|
"app_key":"kzphamrv01",
|
|
"default_lang":"en",
|
|
"token_ttl_seconds":86400,
|
|
"mini":1,
|
|
"app_audio":1,
|
|
"use_lang":1,
|
|
"game_urls":{"1001":"https://dev.playamg.com/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: "amg_1001",
|
|
RoomID: "room_1",
|
|
AccessToken: "app_access_token_amg",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("LaunchGame failed: %v", err)
|
|
}
|
|
if result.ExpiresAtMS != 1700086400000 {
|
|
t.Fatalf("AMG launch must use adapter token ttl, got %+v", result)
|
|
}
|
|
parsed, err := url.Parse(result.LaunchURL)
|
|
if err != nil {
|
|
t.Fatalf("parse AMG launch URL: %v", err)
|
|
}
|
|
if got := parsed.Scheme + "://" + parsed.Host + parsed.Path; got != "https://dev.playamg.com/greedy/index.html" {
|
|
t.Fatalf("AMG launch base mismatch: %s", result.LaunchURL)
|
|
}
|
|
want := map[string]string{
|
|
"user_token": "app_access_token_amg",
|
|
"app_key": "kzphamrv01",
|
|
"room_id": "room_1",
|
|
"mini": "1",
|
|
"lang": "en",
|
|
"app_audio": "1",
|
|
"use_lang": "1",
|
|
}
|
|
for key, value := range want {
|
|
if got := parsed.Query().Get(key); got != value {
|
|
t.Fatalf("AMG query %s = %q, want %q URL=%s", key, got, value, result.LaunchURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandleAMGUserReturnsProfileAndWallet(t *testing.T) {
|
|
token := "amg_user_token"
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: gamedomain.PlatformCodeAMG,
|
|
AdapterType: gamedomain.AdapterAMGV1,
|
|
AdapterConfigJSON: `{"uid_mode":"display_user_id","coin_scale":1}`,
|
|
},
|
|
session: gamedomain.LaunchSession{
|
|
AppCode: "lalu",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
RoomID: "room_1",
|
|
PlatformCode: gamedomain.PlatformCodeAMG,
|
|
GameID: "amg_1001",
|
|
ProviderGameID: "1001",
|
|
LaunchTokenHash: stableHash(token),
|
|
Status: gamedomain.SessionActive,
|
|
ExpiresAtMS: 1700086400000,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 1200}
|
|
svc := New(Config{}, repo, wallet, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
|
|
raw, _, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{AppCode: "lalu", RequestId: "req-amg-user"},
|
|
PlatformCode: gamedomain.PlatformCodeAMG,
|
|
Operation: "user",
|
|
Headers: map[string]string{"Authorization": token},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback failed: %v", err)
|
|
}
|
|
var response struct {
|
|
Code int `json:"code"`
|
|
Data struct {
|
|
UserID string `json:"userId"`
|
|
Nickname string `json:"nickname"`
|
|
AvatarURL string `json:"avatarUrl"`
|
|
AvailableCoins json.Number `json:"availableCoins"`
|
|
RoomID string `json:"roomId"`
|
|
} `json:"data"`
|
|
Message string `json:"msg"`
|
|
}
|
|
if err := json.Unmarshal(raw, &response); err != nil {
|
|
t.Fatalf("decode AMG user response: %v body=%s", err, raw)
|
|
}
|
|
if response.Code != 0 || response.Message != "success" || response.Data.UserID != "420001" || response.Data.Nickname != "tester" || response.Data.AvatarURL == "" || response.Data.AvailableCoins.String() != "1200" || response.Data.RoomID != "room_1" {
|
|
t.Fatalf("AMG user response mismatch: %+v body=%s", response, raw)
|
|
}
|
|
}
|
|
|
|
func TestHandleAMGSettlementDecryptsAndIsIdempotentAcrossRandomCiphertexts(t *testing.T) {
|
|
privateKey, secret := amgTestPrivateKey(t)
|
|
token := "amg_settlement_token"
|
|
repo := &fakeRepository{
|
|
platform: gamedomain.Platform{
|
|
PlatformCode: gamedomain.PlatformCodeAMG,
|
|
AdapterType: gamedomain.AdapterAMGV1,
|
|
CallbackSecretCiphertext: secret,
|
|
AdapterConfigJSON: `{"coin_scale":1}`,
|
|
},
|
|
session: gamedomain.LaunchSession{
|
|
AppCode: "lalu",
|
|
UserID: 42,
|
|
DisplayUserID: "420001",
|
|
RoomID: "room_1",
|
|
RegionID: 100,
|
|
CountryID: 971,
|
|
PlatformCode: gamedomain.PlatformCodeAMG,
|
|
GameID: "amg_1001",
|
|
ProviderGameID: "1001",
|
|
LaunchTokenHash: stableHash(token),
|
|
Status: gamedomain.SessionActive,
|
|
ExpiresAtMS: 1700086400000,
|
|
},
|
|
}
|
|
wallet := &fakeWallet{balanceAfter: 900}
|
|
svc := New(Config{}, repo, wallet, &fakeUser{})
|
|
svc.now = func() time.Time { return time.UnixMilli(1700000000000) }
|
|
plain := []byte(`{"transactionId":"amg-order-1","coins":100,"type":1,"timestamp":1700000000000,"ext":"room_game","gameId":1001}`)
|
|
|
|
for attempt := 0; attempt < 2; attempt++ {
|
|
encrypted := amgTestEncrypt(t, &privateKey.PublicKey, plain)
|
|
envelope, err := json.Marshal(map[string]string{"data": encrypted})
|
|
if err != nil {
|
|
t.Fatalf("marshal envelope: %v", err)
|
|
}
|
|
raw, _, err := svc.HandleCallback(context.Background(), &gamev1.CallbackRequest{
|
|
Meta: &gamev1.RequestMeta{AppCode: "lalu", RequestId: "req-amg-settle-" + strconv.Itoa(attempt)},
|
|
PlatformCode: gamedomain.PlatformCodeAMG,
|
|
Operation: "settlement",
|
|
Headers: map[string]string{"Authorization": token},
|
|
RawBody: envelope,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("HandleCallback attempt %d failed: %v", attempt, err)
|
|
}
|
|
var response struct {
|
|
Code int `json:"code"`
|
|
Data struct {
|
|
AvailableCoins json.Number `json:"availableCoins"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(raw, &response); err != nil || response.Code != 0 || response.Data.AvailableCoins.String() != "900" {
|
|
t.Fatalf("AMG settlement response mismatch: err=%v response=%+v body=%s", err, response, raw)
|
|
}
|
|
}
|
|
if wallet.applyCount != 1 || wallet.lastApply.GetOpType() != "debit" || wallet.lastApply.GetCoinAmount() != 100 || wallet.lastApply.GetProviderOrderId() != "amg-order-1" {
|
|
t.Fatalf("AMG wallet change mismatch: count=%d request=%+v", wallet.applyCount, wallet.lastApply)
|
|
}
|
|
wantHash := stableHash("amg-order-1|100|1|1700000000000|room_game|1001")
|
|
if repo.order.RequestHash != wantHash || repo.order.Status != gamedomain.OrderStatusSucceeded {
|
|
t.Fatalf("AMG order idempotency mismatch: %+v want_hash=%s", repo.order, wantHash)
|
|
}
|
|
}
|
|
|
|
func amgTestPrivateKey(t *testing.T) (*rsa.PrivateKey, string) {
|
|
t.Helper()
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
|
if err != nil {
|
|
t.Fatalf("generate RSA key: %v", err)
|
|
}
|
|
der, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
|
if err != nil {
|
|
t.Fatalf("marshal PKCS#8 key: %v", err)
|
|
}
|
|
return privateKey, base64.StdEncoding.EncodeToString(der)
|
|
}
|
|
|
|
func amgTestEncrypt(t *testing.T, publicKey *rsa.PublicKey, plain []byte) string {
|
|
t.Helper()
|
|
chunkSize := publicKey.Size() - 11
|
|
ciphertext := make([]byte, 0, ((len(plain)+chunkSize-1)/chunkSize)*publicKey.Size())
|
|
for start := 0; start < len(plain); start += chunkSize {
|
|
end := start + chunkSize
|
|
if end > len(plain) {
|
|
end = len(plain)
|
|
}
|
|
chunk, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, plain[start:end])
|
|
if err != nil {
|
|
t.Fatalf("encrypt AMG test chunk: %v", err)
|
|
}
|
|
ciphertext = append(ciphertext, chunk...)
|
|
}
|
|
return hex.EncodeToString(ciphertext)
|
|
}
|