fix: game order api & tests

This commit is contained in:
Ilia Denisov
2026-05-09 10:55:55 +02:00
parent 2a1e80053a
commit 381e41b325
6 changed files with 361 additions and 16 deletions
+7 -8
View File
@@ -46,23 +46,22 @@ type orderParam struct {
func GetOrderHandler(c *gin.Context, executor CommandExecutor) {
p := &orderParam{}
err := c.ShouldBindQuery(p)
if errorResponse(c, err) {
// ShouldBindQuery surfaces both validator failures and strconv parse
// errors; both are client-side faults, so 400 is the correct mapping.
if err := c.ShouldBindQuery(p); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
order, ok, err := executor.FetchOrder(p.Player, uint(p.Turn))
o, ok, err := executor.FetchOrder(p.Player, uint(p.Turn))
if errorResponse(c, err) {
return
}
if !ok {
// there was no order previously sent by player
// no order has been previously stored by the player for this turn
c.Status(http.StatusNoContent)
}
var cmd rest.Command
if errorResponse(c, c.ShouldBindJSON(&cmd)) {
return
}
c.JSON(http.StatusOK, order)
c.JSON(http.StatusOK, o)
}
+168
View File
@@ -2,6 +2,7 @@ package router_test
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
@@ -9,7 +10,9 @@ import (
"galaxy/model/order"
"galaxy/model/rest"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOrderRaceQuit(t *testing.T) {
@@ -940,3 +943,168 @@ func TestMultipleCommandOrder(t *testing.T) {
assert.Equal(t, 2, e.(*dummyExecutor).CommandsExecuted)
}
func TestPutOrderResponseBody(t *testing.T) {
e := &dummyExecutor{
ValidateOrderResult: &order.UserGamesOrder{
GameID: uuid.New(),
UpdatedAt: 1700,
Commands: []order.DecodableCommand{
&order.CommandRaceVote{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceVote},
Acceptor: "Opponent",
},
},
},
}
r := setupRouterExecutor(e)
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandRaceVote{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceVote},
Acceptor: "Opponent",
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
require.Equal(t, http.StatusAccepted, w.Code, w.Body)
var got struct {
GameID uuid.UUID `json:"game_id"`
UpdatedAt int64 `json:"updatedAt"`
Commands []json.RawMessage `json:"cmd"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got))
assert.Equal(t, e.ValidateOrderResult.GameID, got.GameID)
assert.Equal(t, e.ValidateOrderResult.UpdatedAt, got.UpdatedAt)
assert.Len(t, got.Commands, 1)
}
func TestPutOrderEngineError(t *testing.T) {
e := &dummyExecutor{ValidateOrderErr: errors.New("engine boom")}
r := setupRouterExecutor(e)
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandRaceVote{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceVote},
Acceptor: "Opponent",
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code, w.Body)
}
func TestGetOrderQueryValidation(t *testing.T) {
for _, tc := range []struct {
description string
query string
expectStatus int
}{
{"Missing player param", "", http.StatusBadRequest},
{"Empty player", "?player=", http.StatusBadRequest},
{"Blank player", "?player=%20%20%20", http.StatusBadRequest},
{"Negative turn", "?player=Race_01&turn=-1", http.StatusBadRequest},
{"Non-numeric turn", "?player=Race_01&turn=abc", http.StatusBadRequest},
} {
t.Run(tc.description, func(t *testing.T) {
e := &dummyExecutor{}
r := setupRouterExecutor(e)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, apiOrderPath+tc.query, nil)
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
assert.Empty(t, e.FetchOrderActor, "FetchOrder must not be called on validation error")
})
}
}
func TestGetOrderFound(t *testing.T) {
stored := &order.UserGamesOrder{
GameID: uuid.New(),
UpdatedAt: 4242,
Commands: []order.DecodableCommand{
&order.CommandRaceVote{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceVote},
Acceptor: "Opponent",
},
},
}
e := &dummyExecutor{
FetchOrderResult: stored,
FetchOrderOK: true,
}
r := setupRouterExecutor(e)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, apiOrderPath+"?player=Race_01&turn=3", nil)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body)
assert.Equal(t, "Race_01", e.FetchOrderActor)
assert.Equal(t, uint(3), e.FetchOrderTurn)
var got struct {
GameID uuid.UUID `json:"game_id"`
UpdatedAt int64 `json:"updatedAt"`
Commands []json.RawMessage `json:"cmd"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got))
assert.Equal(t, stored.GameID, got.GameID)
assert.Equal(t, stored.UpdatedAt, got.UpdatedAt)
assert.Len(t, got.Commands, 1)
}
func TestGetOrderTurnDefaultsToZero(t *testing.T) {
e := &dummyExecutor{
FetchOrderResult: &order.UserGamesOrder{GameID: uuid.New(), UpdatedAt: 1, Commands: []order.DecodableCommand{}},
FetchOrderOK: true,
}
r := setupRouterExecutor(e)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, apiOrderPath+"?player=Race_01", nil)
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body)
assert.Equal(t, uint(0), e.FetchOrderTurn)
}
func TestGetOrderNotFound(t *testing.T) {
e := &dummyExecutor{FetchOrderOK: false}
r := setupRouterExecutor(e)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, apiOrderPath+"?player=Race_01&turn=2", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusNoContent, w.Code, w.Body)
assert.Empty(t, w.Body.Bytes(), "204 response must carry no body")
assert.Equal(t, "Race_01", e.FetchOrderActor)
assert.Equal(t, uint(2), e.FetchOrderTurn)
}
func TestGetOrderEngineError(t *testing.T) {
e := &dummyExecutor{FetchOrderErr: errors.New("engine boom")}
r := setupRouterExecutor(e)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, apiOrderPath+"?player=Race_01&turn=0", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code, w.Body)
}
+27 -2
View File
@@ -32,15 +32,40 @@ func id() string {
type dummyExecutor struct {
CommandsExecuted int
// ValidateOrderResult, when non-nil, is returned from ValidateOrder.
// When nil, ValidateOrder synthesises an order from the received args
// so the response body is non-empty for status assertions.
ValidateOrderResult *order.UserGamesOrder
ValidateOrderErr error
// FetchOrder controls and observes calls to FetchOrder.
FetchOrderActor string
FetchOrderTurn uint
FetchOrderResult *order.UserGamesOrder
FetchOrderOK bool
FetchOrderErr error
}
func (e *dummyExecutor) ValidateOrder(actor string, cmd ...order.DecodableCommand) (*order.UserGamesOrder, error) {
e.CommandsExecuted = len(cmd)
return nil, nil
if e.ValidateOrderErr != nil {
return nil, e.ValidateOrderErr
}
if e.ValidateOrderResult != nil {
return e.ValidateOrderResult, nil
}
return &order.UserGamesOrder{
GameID: uuid.New(),
UpdatedAt: 1,
Commands: append([]order.DecodableCommand(nil), cmd...),
}, nil
}
func (e *dummyExecutor) FetchOrder(actor string, turn uint) (*order.UserGamesOrder, bool, error) {
return nil, true, nil
e.FetchOrderActor = actor
e.FetchOrderTurn = turn
return e.FetchOrderResult, e.FetchOrderOK, e.FetchOrderErr
}
func (e *dummyExecutor) Execute(command ...handler.Command) error {