347 lines
8.4 KiB
Go
Executable File
347 lines
8.4 KiB
Go
Executable File
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"
|
||
)
|
||
|
||
// ---- 与 handler.go 保持同步的本地类型 ----
|
||
|
||
type settlePlayerRecord struct {
|
||
UserID int64 `json:"user_id"`
|
||
Ticket string `json:"ticket"`
|
||
Win bool `json:"win"`
|
||
Rank int `json:"rank"`
|
||
Score int `json:"score"`
|
||
DamageDealt int `json:"damage_dealt"`
|
||
DamageTaken int `json:"damage_taken"`
|
||
Kills int `json:"kills"`
|
||
ChestsCollected int `json:"chests_collected"`
|
||
RoundsSurvived int `json:"rounds_survived"`
|
||
}
|
||
|
||
type settleRequest struct {
|
||
MatchID string `json:"match_id"`
|
||
GameType string `json:"game_type"`
|
||
TotalRounds int `json:"total_rounds"`
|
||
Players []settlePlayerRecord `json:"players"`
|
||
// 兼容旧版单人字段
|
||
UserID string `json:"user_id"`
|
||
Ticket string `json:"ticket"`
|
||
Win bool `json:"win"`
|
||
Score int `json:"score"`
|
||
}
|
||
|
||
type settleResponse struct {
|
||
Success bool `json:"success"`
|
||
Reward string `json:"reward,omitempty"`
|
||
}
|
||
|
||
// ---- calcRankPoints 本地副本(保持与 handler.go 一致) ----
|
||
|
||
func calcRankPoints(rank int) int64 {
|
||
switch rank {
|
||
case 1:
|
||
return 1000
|
||
case 2:
|
||
return -900
|
||
case 3:
|
||
return -1100
|
||
case 4:
|
||
return -1300
|
||
default:
|
||
return 0
|
||
}
|
||
}
|
||
|
||
// ---- 名次积分单元测试 ----
|
||
|
||
func TestCalcRankPoints(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
rank int
|
||
expected int64
|
||
}{
|
||
{
|
||
name: "第1名固定加分",
|
||
rank: 1,
|
||
expected: 1000,
|
||
},
|
||
{
|
||
name: "第2名固定扣分",
|
||
rank: 2,
|
||
expected: -900,
|
||
},
|
||
{
|
||
name: "第3名固定扣分",
|
||
rank: 3,
|
||
expected: -1100,
|
||
},
|
||
{
|
||
name: "第4名固定扣分",
|
||
rank: 4,
|
||
expected: -1300,
|
||
},
|
||
{
|
||
name: "未知名次兜底为0",
|
||
rank: 0,
|
||
expected: 0,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
got := calcRankPoints(tt.rank)
|
||
assert.Equal(t, tt.expected, got)
|
||
})
|
||
}
|
||
}
|
||
|
||
// ---- 批量结算:免费模式检测 ----
|
||
|
||
func TestSettleGame_FreeModeDetection(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
gameType string
|
||
expectFree bool
|
||
}{
|
||
{"免费模式", "minesweeper_free", true},
|
||
{"付费模式", "minesweeper", false},
|
||
{"空game_type不算免费", "", false},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
isFree := tt.gameType == "minesweeper_free"
|
||
assert.Equal(t, tt.expectFree, isFree)
|
||
})
|
||
}
|
||
}
|
||
|
||
// ---- 幂等:match_id 重复检测 ----
|
||
|
||
func TestSettleGame_Idempotency(t *testing.T) {
|
||
// 模拟:同一 match_id 第二次进来时应跳过
|
||
recorded := map[string]bool{}
|
||
settle := func(matchID string) bool {
|
||
if recorded[matchID] {
|
||
return false // 幂等跳过
|
||
}
|
||
recorded[matchID] = true
|
||
return true
|
||
}
|
||
|
||
assert.True(t, settle("match-001"), "第一次应成功")
|
||
assert.False(t, settle("match-001"), "第二次应被跳过")
|
||
assert.True(t, settle("match-002"), "不同 match_id 应成功")
|
||
}
|
||
|
||
// ---- 批量结算:兼容旧版单人字段 ----
|
||
|
||
func TestSettleGame_BackwardCompatibility(t *testing.T) {
|
||
// 旧版请求(无 players 字段)
|
||
old := settleRequest{
|
||
UserID: "12345",
|
||
Ticket: "GT001",
|
||
MatchID: "match-old",
|
||
Win: true,
|
||
Score: 50,
|
||
GameType: "minesweeper",
|
||
}
|
||
|
||
// 兼容逻辑:players 为空时,从旧版字段构建
|
||
if len(old.Players) == 0 && old.UserID != "" {
|
||
old.Players = []settlePlayerRecord{{
|
||
UserID: 12345,
|
||
Ticket: old.Ticket,
|
||
Win: old.Win,
|
||
Score: old.Score,
|
||
}}
|
||
}
|
||
|
||
assert.Len(t, old.Players, 1)
|
||
assert.Equal(t, int64(12345), old.Players[0].UserID)
|
||
assert.True(t, old.Players[0].Win)
|
||
}
|
||
|
||
// ---- Redis ticket 清理验证 ----
|
||
|
||
func TestSettleGame_TicketCleanup(t *testing.T) {
|
||
mr, err := miniredis.Run()
|
||
assert.NoError(t, err)
|
||
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", 30*time.Minute)
|
||
|
||
// 确认 ticket 存在
|
||
val, err := rdb.Get(ctx, ticketKey).Result()
|
||
assert.NoError(t, err)
|
||
assert.Contains(t, val, "12345")
|
||
|
||
// 结算后清除 ticket
|
||
rdb.Del(ctx, ticketKey)
|
||
|
||
_, err = rdb.Get(ctx, ticketKey).Result()
|
||
assert.Error(t, err, "ticket 应已被清除")
|
||
}
|
||
|
||
// ---- HTTP 集成测试(模拟简化版 settle handler) ----
|
||
|
||
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{
|
||
MatchID: "match-free-001",
|
||
GameType: "minesweeper_free",
|
||
TotalRounds: 10,
|
||
Players: []settlePlayerRecord{
|
||
{UserID: 10001, Win: true, Score: 30, ChestsCollected: 2},
|
||
{UserID: 10002, Win: false, Score: 15},
|
||
},
|
||
},
|
||
expectedStatus: http.StatusOK,
|
||
checkResponse: func(t *testing.T, body []byte) {
|
||
var resp settleResponse
|
||
_ = json.Unmarshal(body, &resp)
|
||
assert.True(t, resp.Success)
|
||
},
|
||
},
|
||
{
|
||
name: "付费模式_批量结算_成功",
|
||
request: settleRequest{
|
||
MatchID: "match-paid-001",
|
||
GameType: "minesweeper",
|
||
TotalRounds: 15,
|
||
Players: []settlePlayerRecord{
|
||
{UserID: 10001, Win: true, Score: 50, DamageDealt: 8, ChestsCollected: 3},
|
||
{UserID: 10002, Win: false, Score: 20, DamageTaken: 6},
|
||
},
|
||
},
|
||
expectedStatus: http.StatusOK,
|
||
checkResponse: func(t *testing.T, body []byte) {
|
||
var resp settleResponse
|
||
_ = json.Unmarshal(body, &resp)
|
||
assert.True(t, resp.Success)
|
||
},
|
||
},
|
||
{
|
||
name: "空players且无旧版字段_直接返回成功",
|
||
request: settleRequest{
|
||
MatchID: "match-empty-001",
|
||
GameType: "minesweeper",
|
||
Players: []settlePlayerRecord{},
|
||
},
|
||
expectedStatus: http.StatusOK,
|
||
checkResponse: func(t *testing.T, body []byte) {
|
||
var resp settleResponse
|
||
_ = json.Unmarshal(body, &resp)
|
||
assert.True(t, resp.Success)
|
||
},
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
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
|
||
}
|
||
|
||
// 兼容旧版
|
||
if len(req.Players) == 0 && req.UserID != "" {
|
||
req.Players = []settlePlayerRecord{{UserID: 12345, Win: req.Win, Rank: 1, Score: req.Score}}
|
||
}
|
||
|
||
if len(req.Players) == 0 {
|
||
c.JSON(http.StatusOK, settleResponse{Success: true})
|
||
return
|
||
}
|
||
|
||
// 计算积分(验证公式被调用)
|
||
// 计算积分(验证名次映射)
|
||
expectedRankPoints := map[int]int64{1: 1000, 2: -900, 3: -1100, 4: -1300}
|
||
for _, p := range req.Players {
|
||
pts := calcRankPoints(p.Rank)
|
||
if expected, ok := expectedRankPoints[p.Rank]; ok {
|
||
assert.Equal(t, expected, pts)
|
||
}
|
||
}
|
||
|
||
c.JSON(http.StatusOK, settleResponse{Success: true})
|
||
})
|
||
|
||
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)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// ---- 旧版单人 Bug 场景:现在通过 players 字段兼容 ----
|
||
|
||
func TestSettleGame_OldBugScenario(t *testing.T) {
|
||
t.Run("旧版单人结算字段兼容", func(t *testing.T) {
|
||
req := settleRequest{
|
||
UserID: "12345",
|
||
Ticket: "GT123",
|
||
GameType: "minesweeper_free",
|
||
Win: true,
|
||
Score: 100,
|
||
}
|
||
|
||
// 新版兼容逻辑
|
||
isFree := req.GameType == "minesweeper_free"
|
||
assert.True(t, isFree, "免费模式通过 game_type 判断,不依赖 Redis")
|
||
|
||
// players 为空时从旧版字段补全
|
||
if len(req.Players) == 0 && req.UserID != "" {
|
||
req.Players = []settlePlayerRecord{{UserID: 12345, Win: req.Win, Score: req.Score}}
|
||
}
|
||
assert.Len(t, req.Players, 1)
|
||
})
|
||
}
|
||
|
||
// ---- 性能基准:积分计算 ----
|
||
|
||
func BenchmarkCalcRankPoints(b *testing.B) {
|
||
for i := 0; i < b.N; i++ {
|
||
calcRankPoints(1)
|
||
}
|
||
}
|