af30846091
Validation of a player's order now applies every command against a transient game-state snapshot and records the per-command outcome (cmdApplied, cmdErrorCode) in each command's meta. The order is persisted even when some commands are rejected, and the response is 202 + UserGamesOrder so clients can surface the partial failure without the chain collapsing into "downstream service is unavailable". Pkg/error consts are reshelved onto three explicit ranges with a package doc and helpers (IsInternalCode/IsInputCode/IsGameStateCode): 1xxx internal/server (500/501), 2xxx structural input (400), 3xxx game-state per-command rejection (400 when escaping HTTP, otherwise recorded as cmdErrorCode). Two pre-existing typos fixed mechanically (ErrBeakGroupNumberNotEnough -> ErrBreakGroupNumberNotEnough, ErrRaceExinct -> ErrRaceExtinct) along with all callsites. Engine errorResponse maps *GenericError by shelf rather than mapping everything to 500. The Quit-not-last structural check in Controller.ValidateOrder is preserved and its type assertion fixed (was a value assertion against a pointer-typed command, so the check silently never fired). Backend, gateway and UI are unchanged — they were already correct on the 202 path; only the engine collapsing per-command rejection into 500 was needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
151 lines
5.3 KiB
Go
151 lines
5.3 KiB
Go
package controller_test
|
|
|
|
import (
|
|
"slices"
|
|
"strconv"
|
|
"testing"
|
|
|
|
e "galaxy/error"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestShipClassCreate(t *testing.T) {
|
|
c, g := newCache()
|
|
|
|
assert.NoError(t, g.ShipClassCreate(Race_0.Name, "Random", 1, 3, 5, 4, 2))
|
|
ships := slices.Collect(c.ListShipTypes(Race_0_idx))
|
|
assert.Len(t, ships, 4)
|
|
st := ships[3]
|
|
assert.Equal(t, 1., float64(st.Drive))
|
|
assert.Equal(t, 3, int(st.Armament))
|
|
assert.Equal(t, 5., float64(st.Weapons))
|
|
assert.Equal(t, 4., float64(st.Shields))
|
|
assert.Equal(t, 2., float64(st.Cargo))
|
|
|
|
assert.ErrorContains(t,
|
|
g.ShipClassCreate(Race_0.Name, Race_0_Gunship, 1, 0, 0, 0, 0),
|
|
e.GenericErrorText(e.ErrInputNewEntityDuplicateIdentifier))
|
|
assert.ErrorContains(t,
|
|
g.ShipClassCreate(UnknownRace, "Drone", 1, 0, 0, 0, 0),
|
|
e.GenericErrorText(e.ErrInputUnknownRace))
|
|
assert.ErrorContains(t,
|
|
g.ShipClassCreate(Race_Extinct.Name, "Drone", 1, 0, 0, 0, 0),
|
|
e.GenericErrorText(e.ErrRaceExtinct))
|
|
assert.ErrorContains(t,
|
|
g.ShipClassCreate(Race_0.Name, BadEntityName, 1, 0, 0, 0, 0),
|
|
e.GenericErrorText(e.ErrInputEntityTypeNameInvalid))
|
|
}
|
|
|
|
func TestCreateShipTypeValidation(t *testing.T) {
|
|
race := Race_0.Name
|
|
typeName := "Drone"
|
|
type tc struct {
|
|
name string
|
|
d, w, s, c float64
|
|
a int
|
|
err string
|
|
}
|
|
table := []tc{
|
|
// correct values
|
|
{typeName, 1, 0, 0, 0, 0, ""},
|
|
{typeName, 1.1, 0, 0, 0, 0, ""},
|
|
{typeName, 1, 1.2, 0, 0, 1, ""},
|
|
{typeName, 1, 1.2, 2.5, 0, 1, ""},
|
|
{typeName, 1, 0, 2.5, 7.7, 0, ""},
|
|
// incorrect values...
|
|
{"", 1, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)},
|
|
{" ", 1, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)},
|
|
{typeName, 0, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputShipTypeZeroValues)},
|
|
// drive
|
|
{typeName, -1, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputDriveValue)},
|
|
{typeName, 0.5, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputDriveValue)},
|
|
// weapons
|
|
{typeName, 0, -1, 0, 0, 0, e.GenericErrorText(e.ErrInputWeaponsValue)},
|
|
{typeName, 0, 0.5, 0, 0, 0, e.GenericErrorText(e.ErrInputWeaponsValue)},
|
|
// shields
|
|
{typeName, 0, 0, -1, 0, 0, e.GenericErrorText(e.ErrInputShieldsValue)},
|
|
{typeName, 0, 0, 0.5, 0, 0, e.GenericErrorText(e.ErrInputShieldsValue)},
|
|
// cargo
|
|
{typeName, 0, 0, 0, -1, 0, e.GenericErrorText(e.ErrInputCargoValue)},
|
|
{typeName, 0, 0, 0, 0.5, 0, e.GenericErrorText(e.ErrInputCargoValue)},
|
|
// armament (and weapons)
|
|
{typeName, 0, 0, 0, 0, -1, e.GenericErrorText(e.ErrInputShipTypeArmamentValue)},
|
|
{typeName, 0, 1, 0, 0, 0, e.GenericErrorText(e.ErrInputShipTypeWeaponsAndArmamentValue)},
|
|
{typeName, 0, 0, 0, 0, 1, e.GenericErrorText(e.ErrInputShipTypeWeaponsAndArmamentValue)},
|
|
}
|
|
for i, tc := range table {
|
|
_, g := newCache()
|
|
|
|
if tc.err == "" {
|
|
err := g.ShipClassCreate(race, tc.name+strconv.Itoa(i), tc.d, tc.a, tc.w, tc.s, tc.c)
|
|
assert.NoError(t, err)
|
|
err = g.ShipClassCreate(race, tc.name+strconv.Itoa(i), tc.d, tc.a, tc.w, tc.s, tc.c)
|
|
assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputNewEntityDuplicateIdentifier))
|
|
} else {
|
|
err := g.ShipClassCreate(race, tc.name, tc.d, tc.a, tc.w, tc.s, tc.c)
|
|
assert.ErrorContains(t, err, tc.err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestShipClassMerge(t *testing.T) {
|
|
c, g := newCache()
|
|
|
|
assert.Len(t, c.ShipTypes(Race_0_idx), 3)
|
|
|
|
assert.NoError(t, g.ShipClassCreate(Race_0.Name, "Drone", 1, 0, 0, 0, 0))
|
|
assert.Len(t, c.ShipTypes(Race_0_idx), 4)
|
|
assert.NoError(t, g.ShipClassCreate(Race_0.Name, "Spy", 1, 0, 0, 0, 0))
|
|
assert.Len(t, c.ShipTypes(Race_0_idx), 5)
|
|
assert.NoError(t, g.ShipClassCreate(Race_0.Name, "Surfer", 15, 15, 15, 0, 1))
|
|
assert.Len(t, c.ShipTypes(Race_0_idx), 6)
|
|
|
|
assert.ErrorContains(t,
|
|
g.ShipClassMerge(Race_0.Name, "Sky", "Drone"),
|
|
e.GenericErrorText(e.ErrInputEntityNotExists))
|
|
assert.ErrorContains(t,
|
|
g.ShipClassMerge(Race_0.Name, "Spy", "Elephant"),
|
|
e.GenericErrorText(e.ErrInputEntityNotExists))
|
|
assert.ErrorContains(t,
|
|
g.ShipClassMerge(Race_Extinct.Name, "Spy", "Drone"),
|
|
e.GenericErrorText(e.ErrRaceExtinct))
|
|
assert.ErrorContains(t,
|
|
g.ShipClassMerge(Race_0.Name, "Spy", "Spy"),
|
|
e.GenericErrorText(e.ErrInputEntityTypeNameEquality))
|
|
|
|
assert.NoError(t, g.ShipClassMerge(Race_0.Name, "Spy", "Drone"))
|
|
assert.Len(t, c.ShipTypes(Race_0_idx), 5)
|
|
|
|
assert.ErrorContains(t,
|
|
g.ShipClassMerge(Race_0.Name, "Drone", "Surfer"),
|
|
e.GenericErrorText(e.ErrMergeShipTypeNotEqual))
|
|
}
|
|
|
|
func TestShipClassRemove(t *testing.T) {
|
|
c, g := newCache()
|
|
|
|
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
|
|
assert.NoError(t, g.ShipClassCreate(Race_0.Name, "Drone", 1, 0, 0, 0, 0))
|
|
g.PlanetProduce(Race_0.Name, int(R0_Planet_0_num), "SHIP", "Drone")
|
|
|
|
assert.ErrorContains(t,
|
|
g.ShipClassRemove(UnknownRace, Race_0_Freighter),
|
|
e.GenericErrorText(e.ErrInputUnknownRace))
|
|
assert.ErrorContains(t,
|
|
g.ShipClassRemove(Race_Extinct.Name, Race_0_Freighter),
|
|
e.GenericErrorText(e.ErrRaceExtinct))
|
|
assert.ErrorContains(t,
|
|
g.ShipClassRemove(Race_0.Name, "Elephant"),
|
|
e.GenericErrorText(e.ErrInputEntityNotExists))
|
|
assert.ErrorContains(t,
|
|
g.ShipClassRemove(Race_0.Name, "Drone"),
|
|
e.GenericErrorText(e.ErrDeleteShipTypePlanetProduction))
|
|
|
|
assert.NoError(t, g.ShipClassRemove(Race_0.Name, Race_0_Freighter))
|
|
|
|
assert.ErrorContains(t,
|
|
g.ShipClassRemove(Race_0.Name, Race_0_Gunship),
|
|
e.GenericErrorText(e.ErrDeleteShipTypeExistingGroup))
|
|
}
|