332 lines
9.3 KiB
Go
332 lines
9.3 KiB
Go
package game_test
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/alicebob/miniredis/v2"
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/redis/go-redis/v9"
|
||
"github.com/stretchr/testify/assert"
|
||
)
|
||
|
||
// settleRequest 结算请求结构体(与 handler.go 保持一致)
|
||
type settleRequest struct {
|
||
UserID string `json:"user_id"`
|
||
Ticket string `json:"ticket"`
|
||
MatchID string `json:"match_id"`
|
||
Win bool `json:"win"`
|
||
Score int `json:"score"`
|
||
GameType string `json:"game_type"`
|
||
}
|
||
|
||
// settleResponse 结算响应结构体
|
||
type settleResponse struct {
|
||
Success bool `json:"success"`
|
||
Reward string `json:"reward,omitempty"`
|
||
}
|
||
|
||
// TestSettleGame_FreeModeDetection 测试免费模式判断逻辑
|
||
// 这是核心测试:验证免费模式通过 game_type 参数判断,而不是依赖 Redis
|
||
func TestSettleGame_FreeModeDetection(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
gameType string
|
||
ticketInRedis bool // 是否在 Redis 中存储 ticket
|
||
expectedReward string // 预期的奖励消息
|
||
shouldBlock bool // 是否应该被拦截(免费模式)
|
||
}{
|
||
{
|
||
name: "免费模式_有ticket_应拦截",
|
||
gameType: "minesweeper_free",
|
||
ticketInRedis: true,
|
||
expectedReward: "体验模式无奖励",
|
||
shouldBlock: true,
|
||
},
|
||
{
|
||
name: "免费模式_无ticket_应拦截",
|
||
gameType: "minesweeper_free",
|
||
ticketInRedis: false,
|
||
expectedReward: "体验模式无奖励",
|
||
shouldBlock: true,
|
||
},
|
||
{
|
||
name: "付费模式_有ticket_应发奖",
|
||
gameType: "minesweeper",
|
||
ticketInRedis: true,
|
||
expectedReward: "", // 付费模式会发放积分奖励
|
||
shouldBlock: false,
|
||
},
|
||
{
|
||
name: "付费模式_无ticket_应发奖",
|
||
gameType: "minesweeper",
|
||
ticketInRedis: false,
|
||
expectedReward: "", // 付费模式会发放积分奖励
|
||
shouldBlock: false,
|
||
},
|
||
{
|
||
name: "空game_type_应发奖",
|
||
gameType: "",
|
||
ticketInRedis: false,
|
||
expectedReward: "", // 空类型不是免费模式
|
||
shouldBlock: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
// 模拟判断逻辑
|
||
isFreeMode := tt.gameType == "minesweeper_free"
|
||
|
||
if tt.shouldBlock {
|
||
assert.True(t, isFreeMode, "免费模式应该被正确识别")
|
||
} else {
|
||
assert.False(t, isFreeMode, "非免费模式不应该被拦截")
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestSettleGame_FreeModeWithRedis 测试 Redis ticket 不影响免费模式判断
|
||
func TestSettleGame_FreeModeWithRedis(t *testing.T) {
|
||
// 1. 启动 miniredis
|
||
mr, err := miniredis.Run()
|
||
assert.NoError(t, err)
|
||
defer mr.Close()
|
||
|
||
rdb := redis.NewClient(&redis.Options{
|
||
Addr: mr.Addr(),
|
||
})
|
||
|
||
ctx := context.Background()
|
||
userID := "12345"
|
||
ticket := "GT123456789"
|
||
|
||
// 场景1: Redis 中有 ticket,但 game_type 是免费模式
|
||
t.Run("Redis有ticket但game_type是免费模式", func(t *testing.T) {
|
||
// 存储 ticket 到 Redis
|
||
ticketKey := fmt.Sprintf("game:token:ticket:%s", ticket)
|
||
rdb.Set(ctx, ticketKey, fmt.Sprintf("%s:minesweeper_free", userID), 30*time.Minute)
|
||
|
||
req := settleRequest{
|
||
UserID: userID,
|
||
Ticket: ticket,
|
||
MatchID: "match-001",
|
||
Win: true,
|
||
Score: 100,
|
||
GameType: "minesweeper_free",
|
||
}
|
||
|
||
// 直接从 req.GameType 判断
|
||
isFreeMode := req.GameType == "minesweeper_free"
|
||
assert.True(t, isFreeMode, "应该识别为免费模式")
|
||
|
||
// 清理
|
||
rdb.Del(ctx, ticketKey)
|
||
})
|
||
|
||
// 场景2: Redis 中没有 ticket(已被删除),但 game_type 是免费模式
|
||
t.Run("Redis无ticket但game_type是免费模式", func(t *testing.T) {
|
||
req := settleRequest{
|
||
UserID: userID,
|
||
Ticket: ticket,
|
||
MatchID: "match-002",
|
||
Win: true,
|
||
Score: 100,
|
||
GameType: "minesweeper_free",
|
||
}
|
||
|
||
// 确认 Redis 中没有 ticket
|
||
ticketKey := fmt.Sprintf("game:token:ticket:%s", ticket)
|
||
_, err := rdb.Get(ctx, ticketKey).Result()
|
||
assert.Error(t, err, "ticket 应该不存在")
|
||
|
||
// 直接从 req.GameType 判断(修复后的逻辑)
|
||
isFreeMode := req.GameType == "minesweeper_free"
|
||
assert.True(t, isFreeMode, "即使 Redis 中没有 ticket,也应该识别为免费模式")
|
||
})
|
||
|
||
// 场景3: Redis 中有 ticket 且是免费模式,但 game_type 参数为空(防止绕过)
|
||
t.Run("Redis标记免费但game_type参数为空", func(t *testing.T) {
|
||
ticketKey := fmt.Sprintf("game:token:ticket:%s", ticket)
|
||
rdb.Set(ctx, ticketKey, fmt.Sprintf("%s:minesweeper_free", userID), 30*time.Minute)
|
||
|
||
req := settleRequest{
|
||
UserID: userID,
|
||
Ticket: ticket,
|
||
MatchID: "match-003",
|
||
Win: true,
|
||
Score: 100,
|
||
GameType: "", // 恶意留空
|
||
}
|
||
|
||
// 使用修复后的逻辑:以请求参数为准
|
||
isFreeMode := req.GameType == "minesweeper_free"
|
||
assert.False(t, isFreeMode, "game_type 为空时不应识别为免费模式")
|
||
|
||
// 注意:这里是一个潜在的安全风险,需要确保游戏服务器正确传递 game_type
|
||
// 建议:可以增加双重校验,从 Redis 读取作为备份
|
||
|
||
rdb.Del(ctx, ticketKey)
|
||
})
|
||
}
|
||
|
||
// TestSettleGame_OldBugScenario 重现并验证旧 bug 已被修复
|
||
func TestSettleGame_OldBugScenario(t *testing.T) {
|
||
// 模拟旧代码的问题场景
|
||
t.Run("旧bug重现_ticket被删除后误判为付费模式", func(t *testing.T) {
|
||
mr, _ := miniredis.Run()
|
||
defer mr.Close()
|
||
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
||
ctx := context.Background()
|
||
|
||
userID := "12345"
|
||
ticket := "GT123456789"
|
||
ticketKey := fmt.Sprintf("game:token:ticket:%s", ticket)
|
||
|
||
// 模拟场景:
|
||
// 1. 用户进入免费游戏,ticket 存入 Redis
|
||
rdb.Set(ctx, ticketKey, fmt.Sprintf("%s:minesweeper_free", userID), 30*time.Minute)
|
||
|
||
// 2. 匹配成功后,ticket 被删除
|
||
rdb.Del(ctx, ticketKey)
|
||
|
||
// 3. 游戏结算时尝试读取 ticket
|
||
_, err := rdb.Get(ctx, ticketKey).Result()
|
||
assert.Error(t, err, "ticket 应该已被删除")
|
||
|
||
// --- 旧代码逻辑(有 bug)---
|
||
oldIsFreeMode := false
|
||
if err == nil {
|
||
// 只有在 Redis 中找到 ticket 时才能判断
|
||
// 这里 err != nil,所以 isFreeMode 保持 false
|
||
}
|
||
assert.False(t, oldIsFreeMode, "旧代码:ticket 被删除后无法判断免费模式")
|
||
|
||
// --- 新代码逻辑(已修复)---
|
||
req := settleRequest{
|
||
UserID: userID,
|
||
Ticket: ticket,
|
||
GameType: "minesweeper_free", // 直接从请求参数获取
|
||
}
|
||
newIsFreeMode := req.GameType == "minesweeper_free"
|
||
assert.True(t, newIsFreeMode, "新代码:直接从 game_type 判断,不受 Redis 影响")
|
||
})
|
||
}
|
||
|
||
// TestSettleGame_Integration 集成测试(模拟完整的 HTTP 请求)
|
||
func TestSettleGame_Integration(t *testing.T) {
|
||
gin.SetMode(gin.TestMode)
|
||
|
||
tests := []struct {
|
||
name string
|
||
request settleRequest
|
||
expectedStatus int
|
||
checkResponse func(t *testing.T, body []byte)
|
||
}{
|
||
{
|
||
name: "免费模式结算_应返回体验模式无奖励",
|
||
request: settleRequest{
|
||
UserID: "12345",
|
||
Ticket: "GT123456789",
|
||
MatchID: "match-001",
|
||
Win: true,
|
||
Score: 100,
|
||
GameType: "minesweeper_free",
|
||
},
|
||
expectedStatus: http.StatusOK,
|
||
checkResponse: func(t *testing.T, body []byte) {
|
||
var resp settleResponse
|
||
err := json.Unmarshal(body, &resp)
|
||
assert.NoError(t, err)
|
||
assert.True(t, resp.Success)
|
||
assert.Equal(t, "体验模式无奖励", resp.Reward)
|
||
},
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
// 创建模拟的 handler(简化版,仅测试免费模式判断逻辑)
|
||
router := gin.New()
|
||
router.POST("/internal/game/settle", func(c *gin.Context) {
|
||
var req settleRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
|
||
// 核心逻辑:直接从请求参数判断
|
||
isFreeMode := req.GameType == "minesweeper_free"
|
||
if isFreeMode {
|
||
c.JSON(http.StatusOK, settleResponse{
|
||
Success: true,
|
||
Reward: "体验模式无奖励",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 付费模式发奖逻辑(简化)
|
||
c.JSON(http.StatusOK, settleResponse{
|
||
Success: true,
|
||
Reward: "100积分",
|
||
})
|
||
})
|
||
|
||
// 发送请求
|
||
body, _ := json.Marshal(tt.request)
|
||
req, _ := http.NewRequest("POST", "/internal/game/settle", bytes.NewBuffer(body))
|
||
req.Header.Set("Content-Type", "application/json")
|
||
|
||
w := httptest.NewRecorder()
|
||
router.ServeHTTP(w, req)
|
||
|
||
assert.Equal(t, tt.expectedStatus, w.Code)
|
||
if tt.checkResponse != nil {
|
||
respBody, _ := io.ReadAll(w.Body)
|
||
tt.checkResponse(t, respBody)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// BenchmarkFreeModeCheck 性能测试:对比新旧实现
|
||
func BenchmarkFreeModeCheck(b *testing.B) {
|
||
// 旧实现:需要查询 Redis
|
||
b.Run("旧实现_Redis查询", func(b *testing.B) {
|
||
mr, _ := miniredis.Run()
|
||
defer mr.Close()
|
||
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
||
ctx := context.Background()
|
||
|
||
ticket := "GT123456789"
|
||
ticketKey := fmt.Sprintf("game:token:ticket:%s", ticket)
|
||
rdb.Set(ctx, ticketKey, "12345:minesweeper_free", 30*time.Minute)
|
||
|
||
b.ResetTimer()
|
||
for i := 0; i < b.N; i++ {
|
||
val, err := rdb.Get(ctx, ticketKey).Result()
|
||
if err == nil {
|
||
_ = val == "12345:minesweeper_free"
|
||
}
|
||
}
|
||
})
|
||
|
||
// 新实现:直接比较字符串
|
||
b.Run("新实现_字符串比较", func(b *testing.B) {
|
||
gameType := "minesweeper_free"
|
||
|
||
b.ResetTimer()
|
||
for i := 0; i < b.N; i++ {
|
||
_ = gameType == "minesweeper_free"
|
||
}
|
||
})
|
||
}
|