package controller_test import ( "errors" "testing" e "galaxy/error" "galaxy/model/order" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestValidateOrderRejectsCommandReferencingMissingShipClass mirrors // the scenario reported in issue #59: an order whose only command // builds a ship of a class that does not exist must not turn into a // generic engine failure. The engine records the rejection in the // command's meta and reports no order-level error so the caller can // persist the partial result and forward it as a per-command status. func TestValidateOrderRejectsCommandReferencingMissingShipClass(t *testing.T) { _, ctl := newCache() cmd := &order.CommandPlanetProduce{ CommandMeta: order.CommandMeta{ CmdID: uuid.NewString(), CmdType: order.CommandTypePlanetProduce, }, Number: int(R0_Planet_0_num), Production: "SHIP", Subject: "Nonexistent", } err := ctl.ValidateOrder(Race_0.Name, cmd) assert.NoError(t, err, "per-command rejection must not become an order-level error") require.NotNil(t, cmd.CmdApplied, "cmdApplied must be set on every processed command") assert.False(t, *cmd.CmdApplied) require.NotNil(t, cmd.CmdErrCode, "cmdErrorCode must be set when the command is rejected") assert.Equal(t, e.ErrInputEntityNotExists, *cmd.CmdErrCode) } // TestValidateOrderContinuesAfterRejection — when one command in an // order is rejected, every remaining command is still validated and // receives its own per-command status. Without this property, the // engine would silently drop the tail of an order on the first // failure, which is exactly what produced the issue #59 symptom. func TestValidateOrderContinuesAfterRejection(t *testing.T) { _, ctl := newCache() rejected := &order.CommandPlanetProduce{ CommandMeta: order.CommandMeta{ CmdID: uuid.NewString(), CmdType: order.CommandTypePlanetProduce, }, Number: int(R0_Planet_0_num), Production: "SHIP", Subject: "Nonexistent", } succeeding := &order.CommandPlanetRename{ CommandMeta: order.CommandMeta{ CmdID: uuid.NewString(), CmdType: order.CommandTypePlanetRename, }, Number: int(R0_Planet_0_num), Name: "Homeworld", } err := ctl.ValidateOrder(Race_0.Name, rejected, succeeding) assert.NoError(t, err) require.NotNil(t, rejected.CmdApplied) assert.False(t, *rejected.CmdApplied) require.NotNil(t, rejected.CmdErrCode) assert.Equal(t, e.ErrInputEntityNotExists, *rejected.CmdErrCode) require.NotNil(t, succeeding.CmdApplied) assert.True(t, *succeeding.CmdApplied) require.NotNil(t, succeeding.CmdErrCode) assert.Equal(t, 0, *succeeding.CmdErrCode) } // TestValidateOrderSimulatesPriorCommands — a later command may // depend on the in-memory state mutation performed by an earlier // command in the same order. Creating a ship class and producing a // ship of that class in the same batch should both succeed because // validation runs the commands against the transient state in // submission order. func TestValidateOrderSimulatesPriorCommands(t *testing.T) { _, ctl := newCache() create := &order.CommandShipClassCreate{ CommandMeta: order.CommandMeta{ CmdID: uuid.NewString(), CmdType: order.CommandTypeShipClassCreate, }, Name: "Drone", Drive: 1, } produce := &order.CommandPlanetProduce{ CommandMeta: order.CommandMeta{ CmdID: uuid.NewString(), CmdType: order.CommandTypePlanetProduce, }, Number: int(R0_Planet_0_num), Production: "SHIP", Subject: "Drone", } err := ctl.ValidateOrder(Race_0.Name, create, produce) assert.NoError(t, err) require.NotNil(t, create.CmdApplied) assert.True(t, *create.CmdApplied) require.NotNil(t, produce.CmdApplied) assert.True(t, *produce.CmdApplied) } // TestValidateOrderRejectsQuitFollowedByCommand — quit must be the // last command in the order; if it is followed by another command, // validation aborts at the order level with a structural error so // the caller can surface HTTP 400. func TestValidateOrderRejectsQuitFollowedByCommand(t *testing.T) { _, ctl := newCache() quit := &order.CommandRaceQuit{ CommandMeta: order.CommandMeta{ CmdID: uuid.NewString(), CmdType: order.CommandTypeRaceQuit, }, } follow := &order.CommandRaceVote{ CommandMeta: order.CommandMeta{ CmdID: uuid.NewString(), CmdType: order.CommandTypeRaceVote, }, Acceptor: Race_1.Name, } err := ctl.ValidateOrder(Race_0.Name, quit, follow) require.Error(t, err) var ge *e.GenericError require.True(t, errors.As(err, &ge), "expected GenericError") assert.Equal(t, e.ErrInputQuitCommandFollowedByCommand, ge.Code) }