Files
galaxy-game/game/internal/router/order_test.go
T
Ilia Denisov bde9d535dc
Tests · UI / test (pull_request) Has been cancelled
Tests · Integration / integration (pull_request) Successful in 1m46s
Tests · Go / test (pull_request) Successful in 2m4s
Tests · Go / test (push) Successful in 2m28s
Tests · UI / test (push) Successful in 3m22s
chore(cleanup): purge /command residuals — fakeEngine, canon golden, openapi
Follow-up tidy after the cross-service /command removal (#73):

- Rename the router test double dummyExecutor -> fakeEngine (and the
  newExecutor / setupRouterExecutor helpers -> newFakeEngine /
  setupRouterEngine): it implements handler.Engine now, "executor" was a
  leftover of the removed adapter. Test-only.

- Regenerate the ui/core canon signing golden onto user.games.order
  (request_user_games_command.json -> request_user_games_order.json, fresh
  canonical bytes + Ed25519 signature) and drop the last
  user.games.command references from the Go/TS tests and docs.

- Align game openapi: CommandRequest.cmd no longer carries minItems: 1. It
  is now used only by PUT /api/v1/order, which accepts an empty batch
  (clearing the player's stored order, equivalent to removing every
  command); the contract test freezes the empty-allowed shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 15:16:17 +02:00

1243 lines
38 KiB
Go

package router_test
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
e "galaxy/error"
"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) {
r := setupRouter()
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandRaceQuit{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceQuit},
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, commandNoErrorsStatus, w.Code, w.Body)
// error: actor not set
payload.Actor = ""
w = httptest.NewRecorder()
req, _ = http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
payload.Actor = " "
w = httptest.NewRecorder()
req, _ = http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
// unrecognized command type
payload.Commands = []json.RawMessage{
encodeCommand(&order.CommandRaceQuit{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandType("-unknown-")},
}),
}
w = httptest.NewRecorder()
req, _ = http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
// empty cmd[] is a valid PUT — the player cleared their draft;
// the engine stores the empty batch and answers with the
// canonical `UserGamesOrder` envelope. ValidateOrder receives a
// zero-length variadic and the response carries no commands.
payload = &rest.Command{
Actor: commandDefaultActor,
}
exec := &fakeEngine{}
emptyRouter := setupRouterEngine(exec)
w = httptest.NewRecorder()
req, _ = http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
emptyRouter.ServeHTTP(w, req)
assert.Equal(t, commandNoErrorsStatus, w.Code, w.Body)
assert.Equal(t, 0, exec.CommandsExecuted)
var stored order.UserGamesOrder
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &stored))
assert.Empty(t, stored.Commands)
}
func TestOrderRaceVote(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
acceptor string
}{
{commandNoErrorsStatus, "Valid request", "AnotherRace"},
{http.StatusBadRequest, "Empty acceptor", ""},
{http.StatusBadRequest, "Blank acceptor", " "},
{http.StatusBadRequest, "Invalid acceptor", "Race_👽"},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandRaceVote{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceVote},
Acceptor: tc.acceptor,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderRaceRelation(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
relation string
acceptor string
}{
{commandNoErrorsStatus, "Valid request 1", "WAR", "Opponent"},
{commandNoErrorsStatus, "Valid request 2", "PEACE", "Opponent"},
{http.StatusBadRequest, "Empty relation", "", "Opponent"},
{http.StatusBadRequest, "Blank relation", " ", "Opponent"},
{http.StatusBadRequest, "Invalid relation", "Woina", "Opponent"},
{http.StatusBadRequest, "Empty acceptor", "WAR", ""},
{http.StatusBadRequest, "Blank acceptor", "WAR", " "},
{http.StatusBadRequest, "Invalid acceptor", "PEACE", "Race_👽"},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandRaceRelation{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceRelation},
Acceptor: tc.acceptor,
Relation: tc.relation,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderShipClassCreate(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
D float64
A int
W, S, C float64
name string
expectStatus int
description string
}{
{1, 0, 0, 0, 0, "Drone", commandNoErrorsStatus, "Simple Drone"},
{1, 1, 1, 0, 0, "Drone", commandNoErrorsStatus, "Armed Drone"},
{1, 0, 0, 1, 0, "Drone", commandNoErrorsStatus, "Shielded Drone"},
{1, 0, 0, 0, 1, "Drone", commandNoErrorsStatus, "Carrying Drone"},
{1, 0, 0, 0, 0, "", http.StatusBadRequest, "Empty name"},
{1, 0, 0, 0, 0, " ", http.StatusBadRequest, "Blank name"},
{1, 0, 0, 0, 0, "Drone🚀", http.StatusBadRequest, "Invalid name"},
{-0.5, 0, 0, 0, 0, "Drone", http.StatusBadRequest, "Drive less than 0"},
{0.9, 0, 0, 0, 0, "Drone", http.StatusBadRequest, "Drive less than 1"},
{1, 1, 0, 0, 0, "Drone", http.StatusBadRequest, "Ammo without Weapons"},
{1, 0, 1, 0, 0, "Drone", http.StatusBadRequest, "Weapons without Ammo"},
{1, -1, 1, 0, 0, "Drone", http.StatusBadRequest, "Ammo less than 0"},
{1, 1, 0.9, 0, 0, "Drone", http.StatusBadRequest, "Weapons less than 1"},
{1, 1, -0.5, 0, 0, "Drone", http.StatusBadRequest, "Weapons less than 0"},
{1, 0, 0, -0.5, 0, "Drone", http.StatusBadRequest, "Shields less than 0"},
{1, 0, 0, 0.9, 0, "Drone", http.StatusBadRequest, "Shields less than 1"},
{1, 0, 0, 0, -0.5, "Drone", http.StatusBadRequest, "Cargo less than 0"},
{1, 0, 0, 0, 0.9, "Drone", http.StatusBadRequest, "Cargo less than 1"},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandShipClassCreate{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipClassCreate},
Name: tc.name,
Drive: tc.D,
Armament: tc.A,
Weapons: tc.W,
Shields: tc.S,
Cargo: tc.C,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderShipClassMerge(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
name string
target string
}{
{commandNoErrorsStatus, "Valid request", "Drone", "Spy"},
{http.StatusBadRequest, "Empty name", "", "Spy"},
{http.StatusBadRequest, "Blank name", " ", "Spy"},
{http.StatusBadRequest, "Invalid name", "Drone🚀", "Spy"},
{http.StatusBadRequest, "Empty name", "Drone", " "},
{http.StatusBadRequest, "Blank name", "Drone", " "},
{http.StatusBadRequest, "Invalid name", "Drone", "Spy🚀"},
{http.StatusBadRequest, "Equal names", "Drone", "Drone"},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandShipClassMerge{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipClassMerge},
Name: tc.name,
Target: tc.target,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderShipClassRemove(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
name string
}{
{commandNoErrorsStatus, "Valid request", "Drone"},
{http.StatusBadRequest, "Empty name", ""},
{http.StatusBadRequest, "Blank name", " "},
{http.StatusBadRequest, "Invalid name", "Drone🚀"},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandShipClassRemove{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipClassRemove},
Name: tc.name,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderShipGroupBreak(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
id string
newId string
quantity int
}{
{commandNoErrorsStatus, "Valid request #1", validId1, validId2, 1},
{commandNoErrorsStatus, "Valid request #2", validId1, validId2, 0},
{http.StatusBadRequest, "Negative quantity", validId1, validId2, -1},
{http.StatusBadRequest, "Empty id", "", validId2, 1},
{http.StatusBadRequest, "Invalid id", invalidId, validId2, 1},
{http.StatusBadRequest, "Empty newId", validId1, "", 1},
{http.StatusBadRequest, "Invalid newId", validId1, invalidId, 1},
{http.StatusBadRequest, "Equal id and newId", validId1, validId1, 1},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandShipGroupBreak{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupBreak},
ID: tc.id,
NewID: tc.newId,
Quantity: tc.quantity,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderShipGroupLoad(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
id string
cargo string
quantity float64
}{
{commandNoErrorsStatus, "Valid request #1", validId1, "COL", 0},
{commandNoErrorsStatus, "Valid request #2", validId1, "MAT", 1},
{commandNoErrorsStatus, "Valid request #2", validId1, "CAP", 2},
{http.StatusBadRequest, "Invalid quantity", validId1, "COL", -0.5},
{http.StatusBadRequest, "Empty cargo", validId1, "", 1},
{http.StatusBadRequest, "Invalid cargo", validId1, "IND", 1},
{http.StatusBadRequest, "Empty id", "", "COL", 1},
{http.StatusBadRequest, "Invalid id", invalidId, "COL", 1},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandShipGroupLoad{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupLoad},
ID: tc.id,
Cargo: tc.cargo,
Quantity: tc.quantity,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderShipGroupUnload(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
id string
quantity float64
}{
{commandNoErrorsStatus, "Valid request #1", validId1, 0},
{commandNoErrorsStatus, "Valid request #2", validId1, 1},
{http.StatusBadRequest, "Invalid quantity", validId1, -0.5},
{http.StatusBadRequest, "Empty id", "", 1},
{http.StatusBadRequest, "Invalid id", invalidId, 1},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandShipGroupUnload{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupUnload},
ID: tc.id,
Quantity: tc.quantity,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderShipGroupSend(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
id string
destination int
}{
{commandNoErrorsStatus, "Valid request #1", validId1, 0},
{commandNoErrorsStatus, "Valid request #1", validId1, 1},
{http.StatusBadRequest, "Invalid destination", validId1, -1},
{http.StatusBadRequest, "Empty id", "", 1},
{http.StatusBadRequest, "Invalid id", invalidId, 1},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandShipGroupSend{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupSend},
ID: tc.id,
Destination: tc.destination,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderShipGroupUpgrade(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
id string
tech string
level float64
}{
{commandNoErrorsStatus, "Valid request #1", validId1, "ALL", 0},
{commandNoErrorsStatus, "Valid request #1", validId1, "DRIVE", 1.1},
{commandNoErrorsStatus, "Valid request #1", validId1, "WEAPONS", 2.1},
{commandNoErrorsStatus, "Valid request #1", validId1, "SHIELDS", 3.1},
{commandNoErrorsStatus, "Valid request #1", validId1, "CARGO", 4.1},
{http.StatusBadRequest, "Negative level", validId1, "DRIVE", -0.5},
{http.StatusBadRequest, "Invalid level 0.5", validId1, "DRIVE", 0.5},
{http.StatusBadRequest, "Invalid level 1.0", validId1, "DRIVE", 1.0},
{http.StatusBadRequest, "Empty id", "", "ALL", 0},
{http.StatusBadRequest, "Invalid id", invalidId, "ALL", 0},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandShipGroupUpgrade{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupUpgrade},
ID: tc.id,
Tech: tc.tech,
Level: tc.level,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderShipGroupMerge(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
}{
{commandNoErrorsStatus, "Valid request"},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandShipGroupMerge{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupMerge},
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderShipGroupDismantle(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
id string
}{
{commandNoErrorsStatus, "Valid request", validId1},
{http.StatusBadRequest, "Empty id", ""},
{http.StatusBadRequest, "Invalid id", invalidId},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandShipGroupDismantle{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupDismantle},
ID: tc.id,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderShipGroupTransfer(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
id string
acceptor string
}{
{commandNoErrorsStatus, "Valid request", validId1, "AnotherRace"},
{http.StatusBadRequest, "Blank id", "", "AnotherRace"},
{http.StatusBadRequest, "Invalid id", invalidId, "AnotherRace"},
{http.StatusBadRequest, "Empty acceptor", validId1, ""},
{http.StatusBadRequest, "Blank acceptor", validId1, " "},
{http.StatusBadRequest, "Invalid acceptor", validId1, "Race_👽"},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandShipGroupTransfer{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupTransfer},
ID: tc.id,
Acceptor: tc.acceptor,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderShipGroupJoinFleet(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
id string
name string
}{
{commandNoErrorsStatus, "Valid request", validId1, "AnotherRace"},
{http.StatusBadRequest, "Blank id", "", "AnotherRace"},
{http.StatusBadRequest, "Invalid id", invalidId, "AnotherRace"},
{http.StatusBadRequest, "Empty name", validId1, ""},
{http.StatusBadRequest, "Blank name", validId1, " "},
{http.StatusBadRequest, "Invalid name", validId1, "Fleet_🚢"},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandShipGroupJoinFleet{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipGroupJoinFleet},
ID: tc.id,
Name: tc.name,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderFleetMerge(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
name string
target string
}{
{commandNoErrorsStatus, "Valid request", "Fleet", "Bomber"},
{http.StatusBadRequest, "Empty name", "", "Bomber"},
{http.StatusBadRequest, "Invalid name", "Fleet_🚢", "Bomber"},
{http.StatusBadRequest, "Empty target", "Fleet", ""},
{http.StatusBadRequest, "Invalid target", "Fleet", "Bomber_🚢"},
{http.StatusBadRequest, "Equal name and target", "Fleet", "Fleet"},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandFleetMerge{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeFleetMerge},
Name: tc.name,
Target: tc.target,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderFleetSend(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
name string
destination int
}{
{commandNoErrorsStatus, "Valid request #1", "Fleet", 0},
{commandNoErrorsStatus, "Valid request #2", "Fleet", 1},
{http.StatusBadRequest, "Invalid destination", "Fleet", -1},
{http.StatusBadRequest, "Empty name", "", 1},
{http.StatusBadRequest, "Invalid name", "Fleet_🚢", 1},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandFleetSend{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeFleetSend},
Name: tc.name,
Destination: tc.destination,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderScienceCreate(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
D, W, S, C float64
name string
}{
{commandNoErrorsStatus, "Valid request", 0.25, 0.25, 0.25, 0.25, "Science"},
{http.StatusBadRequest, "Empty name", 0.25, 0.25, 0.25, 0.25, ""},
{http.StatusBadRequest, "Invalid name", 0.25, 0.25, 0.25, 0.25, "Science🧪"},
{http.StatusBadRequest, "Negative drive", -.5, 0.25, 0.25, 0.25, "Science"},
{http.StatusBadRequest, "Negative weapons", 0.25, -.5, 0.25, 0.25, "Science"},
{http.StatusBadRequest, "Negative shields", 0.25, 0.25, -.5, 0.25, "Science"},
{http.StatusBadRequest, "Negative cargo", 0.25, 0.25, 0.25, -.5, "Science"},
{http.StatusBadRequest, "Too big drive", 1.1, 0.25, 0.25, 0.25, "Science"},
{http.StatusBadRequest, "Too big weapons", 0.25, 1.05, 0.25, 0.25, "Science"},
{http.StatusBadRequest, "Too big shields", 0.25, 0.25, 1.5, 0.25, "Science"},
{http.StatusBadRequest, "Too big cargo", 0.25, 0.25, 0.25, 1.01, "Science"},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandScienceCreate{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeScienceCreate},
Name: tc.name,
Drive: tc.D,
Weapons: tc.W,
Shields: tc.S,
Cargo: tc.C,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderScienceRemove(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
name string
}{
{commandNoErrorsStatus, "Valid request", "Drone"},
{http.StatusBadRequest, "Empty name", ""},
{http.StatusBadRequest, "Blank name", " "},
{http.StatusBadRequest, "Invalid name", "Science🧪"},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandScienceRemove{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeScienceRemove},
Name: tc.name,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderPlanetRename(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
number int
name string
}{
{commandNoErrorsStatus, "Valid request #1", 0, "HW"},
{commandNoErrorsStatus, "Valid request #2", 1, "HW"},
{http.StatusBadRequest, "Invalid number", -1, "HW"},
{http.StatusBadRequest, "Empty name", 1, ""},
{http.StatusBadRequest, "Blank name", 1, " "},
{http.StatusBadRequest, "Invalid name", 1, "Planet🪐"},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandPlanetRename{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypePlanetRename},
Number: tc.number,
Name: tc.name,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderPlanetProduce(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
number int
production, subject string
}{
{commandNoErrorsStatus, "Valid request MAT", 0, "MAT", ""},
{commandNoErrorsStatus, "Valid request CAP", 1, "CAP", ""},
{commandNoErrorsStatus, "Valid request DRIVE", 2, "DRIVE", ""},
{commandNoErrorsStatus, "Valid request WEAPONS", 3, "WEAPONS", ""},
{commandNoErrorsStatus, "Valid request SHIELDS", 4, "SHIELDS", ""},
{commandNoErrorsStatus, "Valid request CARGO", 5, "CARGO", ""},
{commandNoErrorsStatus, "Valid request SCIENCE", 6, "SCIENCE", "Science"},
{commandNoErrorsStatus, "Valid request SHIP", 7, "SHIP", "Ship"},
{http.StatusBadRequest, "Empty production", 0, "", ""},
{http.StatusBadRequest, "Invalid production", 0, "IND", ""},
{http.StatusBadRequest, "Invalid planet", -1, "DRIVE", ""},
{http.StatusBadRequest, "Empty science subject", 6, "SCIENCE", ""},
{http.StatusBadRequest, "Invalid science subject", 6, "SCIENCE", "Science🧪"},
{http.StatusBadRequest, "Empty ship subject", 6, "SHIP", ""},
{http.StatusBadRequest, "Invalid ship subject", 6, "SHIP", "Ship🚀"},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandPlanetProduce{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypePlanetProduce},
Number: tc.number,
Production: tc.production,
Subject: tc.subject,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderPlanetRouteSet(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
origin, destination int
loadType string
}{
{commandNoErrorsStatus, "Valid request MAT", 1, 0, "MAT"},
{commandNoErrorsStatus, "Valid request CAP", 0, 1, "CAP"},
{commandNoErrorsStatus, "Valid request COL", 1, 2, "COL"},
{commandNoErrorsStatus, "Valid request EMP", 3, 0, "EMP"},
{http.StatusBadRequest, "Empty loadType", 0, 1, ""},
{http.StatusBadRequest, "Invalid loadType", 0, 1, "IND"},
{http.StatusBadRequest, "Invalid origin", -1, 1, "MAT"},
{http.StatusBadRequest, "Invalid destination", 1, -1, "MAT"},
{http.StatusBadRequest, "Origin equals destination", 1, 1, "COL"},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandPlanetRouteSet{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypePlanetRouteSet},
Origin: tc.origin,
Destination: tc.destination,
LoadType: tc.loadType,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestOrderPlanetRouteRemove(t *testing.T) {
r := setupRouter()
for _, tc := range []struct {
expectStatus int
description string
origin int
loadType string
}{
{commandNoErrorsStatus, "Valid request MAT", 0, "MAT"},
{commandNoErrorsStatus, "Valid request CAP", 1, "CAP"},
{commandNoErrorsStatus, "Valid request COL", 2, "COL"},
{commandNoErrorsStatus, "Valid request EMP", 0, "EMP"},
{http.StatusBadRequest, "Empty loadType", 1, ""},
{http.StatusBadRequest, "Invalid loadType", 1, "IND"},
{http.StatusBadRequest, "Invalid origin", -1, "MAT"},
} {
t.Run(tc.description, func(t *testing.T) {
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandPlanetRouteRemove{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypePlanetRouteRemove},
Origin: tc.origin,
LoadType: tc.loadType,
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestMultipleCommandOrder(t *testing.T) {
e := newFakeEngine()
r := setupRouterEngine(e)
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandRaceRelation{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceRelation},
Acceptor: "Opponent",
Relation: "PEACE",
}),
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, commandNoErrorsStatus, w.Code, w.Body)
assert.Equal(t, 2, e.(*fakeEngine).CommandsExecuted)
}
func TestPutOrderResponseBody(t *testing.T) {
e := &fakeEngine{
ValidateOrderResult: &order.UserGamesOrder{
GameID: uuid.New(),
UpdatedAt: 1700,
Commands: []order.DecodableCommand{
&order.CommandRaceVote{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceVote},
Acceptor: "Opponent",
},
},
},
}
r := setupRouterEngine(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 := &fakeEngine{ValidateOrderErr: errors.New("engine boom")}
r := setupRouterEngine(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)
}
// TestPutOrderPerCommandRejection — when the engine returns an order
// where some commands carry `cmdErrorCode != 0`, the handler must
// still answer 202 with the full UserGamesOrder body so the client
// can surface the per-command failure (rather than treating the
// response as a generic error).
func TestPutOrderPerCommandRejection(t *testing.T) {
applied := true
rejectedFlag := false
rejectedCode := e.ErrInputEntityNotExists
zero := 0
result := &order.UserGamesOrder{
GameID: uuid.New(),
UpdatedAt: 4242,
Commands: []order.DecodableCommand{
&order.CommandShipClassCreate{
CommandMeta: order.CommandMeta{
CmdID: id(),
CmdType: order.CommandTypeShipClassCreate,
CmdApplied: &applied,
CmdErrCode: &zero,
},
Name: "Drone",
Drive: 1,
},
&order.CommandPlanetProduce{
CommandMeta: order.CommandMeta{
CmdID: id(),
CmdType: order.CommandTypePlanetProduce,
CmdApplied: &rejectedFlag,
CmdErrCode: &rejectedCode,
},
Number: 0,
Production: "SHIP",
Subject: "Nonexistent",
},
},
}
executor := &fakeEngine{ValidateOrderResult: result}
r := setupRouterEngine(executor)
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandShipClassCreate{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeShipClassCreate},
Name: "Drone",
Drive: 1,
}),
encodeCommand(&order.CommandPlanetProduce{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypePlanetProduce},
Number: 0,
Production: "SHIP",
Subject: "Nonexistent",
}),
},
}
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))
require.Len(t, got.Commands, 2)
var first struct {
CmdApplied *bool `json:"cmdApplied"`
CmdErrCode *int `json:"cmdErrorCode"`
}
require.NoError(t, json.Unmarshal(got.Commands[0], &first))
require.NotNil(t, first.CmdApplied)
assert.True(t, *first.CmdApplied)
var second struct {
CmdApplied *bool `json:"cmdApplied"`
CmdErrCode *int `json:"cmdErrorCode"`
}
require.NoError(t, json.Unmarshal(got.Commands[1], &second))
require.NotNil(t, second.CmdApplied)
assert.False(t, *second.CmdApplied)
require.NotNil(t, second.CmdErrCode)
assert.Equal(t, e.ErrInputEntityNotExists, *second.CmdErrCode)
}
// TestPutOrderStructuralRejection — order-level structural errors
// (e.g. `quit` not the last command) come back from the executor as a
// *GenericError on the input shelf, which must map to HTTP 400 with
// the `{"generic_error","code"}` envelope rather than 500.
func TestPutOrderStructuralRejection(t *testing.T) {
executor := &fakeEngine{ValidateOrderErr: e.NewQuitCommandFollowedByCommandError()}
r := setupRouterEngine(executor)
payload := &rest.Command{
Actor: commandDefaultActor,
Commands: []json.RawMessage{
encodeCommand(&order.CommandRaceQuit{
CommandMeta: order.CommandMeta{CmdID: id(), CmdType: order.CommandTypeRaceQuit},
}),
},
}
w := httptest.NewRecorder()
req, _ := http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
require.Equal(t, http.StatusBadRequest, w.Code, w.Body)
var got struct {
Code int `json:"code"`
Msg string `json:"generic_error"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got))
assert.Equal(t, e.ErrInputQuitCommandFollowedByCommand, got.Code)
assert.NotEmpty(t, got.Msg)
}
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 := &fakeEngine{}
r := setupRouterEngine(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 := &fakeEngine{
FetchOrderResult: stored,
FetchOrderOK: true,
}
r := setupRouterEngine(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 := &fakeEngine{
FetchOrderResult: &order.UserGamesOrder{GameID: uuid.New(), UpdatedAt: 1, Commands: []order.DecodableCommand{}},
FetchOrderOK: true,
}
r := setupRouterEngine(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 := &fakeEngine{FetchOrderOK: false}
r := setupRouterEngine(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 := &fakeEngine{FetchOrderErr: errors.New("engine boom")}
r := setupRouterEngine(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)
}