ui: plan 01-27 done #1
@@ -0,0 +1,152 @@
|
|||||||
|
package router_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"galaxy/model/report"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetBattleValidation(t *testing.T) {
|
||||||
|
validUUID := uuid.New().String()
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
description string
|
||||||
|
turn string
|
||||||
|
battleID string
|
||||||
|
expectStatus int
|
||||||
|
}{
|
||||||
|
{"Negative turn", "-1", validUUID, http.StatusBadRequest},
|
||||||
|
{"Non-numeric turn", "abc", validUUID, http.StatusBadRequest},
|
||||||
|
{"Invalid uuid", "0", invalidId, http.StatusBadRequest},
|
||||||
|
} {
|
||||||
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
|
e := &dummyExecutor{}
|
||||||
|
r := setupRouterExecutor(e)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
path := fmt.Sprintf("/api/v1/battle/%s/%s", tc.turn, tc.battleID)
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, path, nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
|
||||||
|
assert.Equal(t, uuid.Nil, e.FetchBattleID, "FetchBattle must not be called on validation error")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetBattleFound(t *testing.T) {
|
||||||
|
id := uuid.New()
|
||||||
|
raceA := uuid.New()
|
||||||
|
raceB := uuid.New()
|
||||||
|
stored := &report.BattleReport{
|
||||||
|
ID: id,
|
||||||
|
Planet: 42,
|
||||||
|
PlanetName: "X-Prime",
|
||||||
|
Races: map[int]uuid.UUID{
|
||||||
|
0: raceA,
|
||||||
|
1: raceB,
|
||||||
|
},
|
||||||
|
Ships: map[int]report.BattleReportGroup{
|
||||||
|
10: {
|
||||||
|
Race: "Alpha",
|
||||||
|
ClassName: "Drone",
|
||||||
|
Tech: map[string]report.Float{"WEAPONS": report.F(1)},
|
||||||
|
Number: 5,
|
||||||
|
NumberLeft: 3,
|
||||||
|
LoadType: "EMP",
|
||||||
|
LoadQuantity: report.F(0),
|
||||||
|
InBattle: true,
|
||||||
|
},
|
||||||
|
20: {
|
||||||
|
Race: "Beta",
|
||||||
|
ClassName: "Spy",
|
||||||
|
Tech: map[string]report.Float{"SHIELDS": report.F(2)},
|
||||||
|
Number: 4,
|
||||||
|
NumberLeft: 0,
|
||||||
|
LoadType: "EMP",
|
||||||
|
LoadQuantity: report.F(0),
|
||||||
|
InBattle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Protocol: []report.BattleActionReport{
|
||||||
|
{Attacker: 0, AttackerShipClass: 10, Defender: 1, DefenderShipClass: 20, Destroyed: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
e := &dummyExecutor{
|
||||||
|
FetchBattleResult: stored,
|
||||||
|
FetchBattleOK: true,
|
||||||
|
}
|
||||||
|
r := setupRouterExecutor(e)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
path := fmt.Sprintf("/api/v1/battle/%d/%s", 7, id.String())
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, path, nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, w.Code, w.Body)
|
||||||
|
assert.Equal(t, uint(7), e.FetchBattleTurn)
|
||||||
|
assert.Equal(t, id, e.FetchBattleID)
|
||||||
|
|
||||||
|
var got report.BattleReport
|
||||||
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got))
|
||||||
|
assert.Equal(t, stored.ID, got.ID)
|
||||||
|
assert.Equal(t, stored.Planet, got.Planet)
|
||||||
|
assert.Equal(t, stored.PlanetName, got.PlanetName)
|
||||||
|
assert.Equal(t, stored.Races, got.Races)
|
||||||
|
require.Len(t, got.Ships, len(stored.Ships))
|
||||||
|
assert.Equal(t, stored.Ships[10].ClassName, got.Ships[10].ClassName)
|
||||||
|
assert.Equal(t, stored.Ships[20].NumberLeft, got.Ships[20].NumberLeft)
|
||||||
|
require.Len(t, got.Protocol, 1)
|
||||||
|
assert.Equal(t, stored.Protocol[0], got.Protocol[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetBattleTurnZero(t *testing.T) {
|
||||||
|
id := uuid.New()
|
||||||
|
e := &dummyExecutor{
|
||||||
|
FetchBattleResult: &report.BattleReport{ID: id},
|
||||||
|
FetchBattleOK: true,
|
||||||
|
}
|
||||||
|
r := setupRouterExecutor(e)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/battle/0/%s", id.String()), nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, w.Code, w.Body)
|
||||||
|
assert.Equal(t, uint(0), e.FetchBattleTurn)
|
||||||
|
assert.Equal(t, id, e.FetchBattleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetBattleNotFound(t *testing.T) {
|
||||||
|
id := uuid.New()
|
||||||
|
e := &dummyExecutor{FetchBattleOK: false}
|
||||||
|
r := setupRouterExecutor(e)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/battle/3/%s", id.String()), nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, w.Code, w.Body)
|
||||||
|
assert.Equal(t, uint(3), e.FetchBattleTurn)
|
||||||
|
assert.Equal(t, id, e.FetchBattleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetBattleEngineError(t *testing.T) {
|
||||||
|
e := &dummyExecutor{FetchBattleErr: errors.New("engine boom")}
|
||||||
|
r := setupRouterExecutor(e)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/battle/3/%s", uuid.NewString()), nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code, w.Body)
|
||||||
|
}
|
||||||
@@ -45,6 +45,13 @@ type dummyExecutor struct {
|
|||||||
FetchOrderResult *order.UserGamesOrder
|
FetchOrderResult *order.UserGamesOrder
|
||||||
FetchOrderOK bool
|
FetchOrderOK bool
|
||||||
FetchOrderErr error
|
FetchOrderErr error
|
||||||
|
|
||||||
|
// FetchBattle controls and observes calls to FetchBattle.
|
||||||
|
FetchBattleTurn uint
|
||||||
|
FetchBattleID uuid.UUID
|
||||||
|
FetchBattleResult *report.BattleReport
|
||||||
|
FetchBattleOK bool
|
||||||
|
FetchBattleErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *dummyExecutor) ValidateOrder(actor string, cmd ...order.DecodableCommand) (*order.UserGamesOrder, error) {
|
func (e *dummyExecutor) ValidateOrder(actor string, cmd ...order.DecodableCommand) (*order.UserGamesOrder, error) {
|
||||||
@@ -69,7 +76,9 @@ func (e *dummyExecutor) FetchOrder(actor string, turn uint) (*order.UserGamesOrd
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *dummyExecutor) FetchBattle(turn uint, ID uuid.UUID) (*report.BattleReport, bool, error) {
|
func (e *dummyExecutor) FetchBattle(turn uint, ID uuid.UUID) (*report.BattleReport, bool, error) {
|
||||||
return nil, false, nil
|
e.FetchBattleTurn = turn
|
||||||
|
e.FetchBattleID = ID
|
||||||
|
return e.FetchBattleResult, e.FetchBattleOK, e.FetchBattleErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *dummyExecutor) Execute(command ...handler.Command) error {
|
func (e *dummyExecutor) Execute(command ...handler.Command) error {
|
||||||
|
|||||||
@@ -207,6 +207,33 @@ paths:
|
|||||||
$ref: "#/components/responses/ValidationError"
|
$ref: "#/components/responses/ValidationError"
|
||||||
"500":
|
"500":
|
||||||
$ref: "#/components/responses/InternalError"
|
$ref: "#/components/responses/InternalError"
|
||||||
|
/api/v1/battle/{turn}/{uuid}:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- PlayerActions
|
||||||
|
operationId: getBattle
|
||||||
|
summary: Fetch a single battle report
|
||||||
|
description: |
|
||||||
|
Returns the full `BattleReport` for the supplied `turn` and battle
|
||||||
|
identifier. The `turn` segment must be a non-negative integer; the
|
||||||
|
`uuid` segment must be a valid RFC 4122 UUID. Responds with
|
||||||
|
`404 Not Found` when no battle is stored for the supplied pair.
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/BattleTurnParam"
|
||||||
|
- $ref: "#/components/parameters/BattleIDParam"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Battle report for the supplied turn and identifier.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/BattleReport"
|
||||||
|
"400":
|
||||||
|
$ref: "#/components/responses/ValidationError"
|
||||||
|
"404":
|
||||||
|
description: No battle exists for the supplied turn and identifier.
|
||||||
|
"500":
|
||||||
|
$ref: "#/components/responses/InternalError"
|
||||||
/api/v1/admin/turn:
|
/api/v1/admin/turn:
|
||||||
put:
|
put:
|
||||||
tags:
|
tags:
|
||||||
@@ -265,6 +292,22 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
minimum: 0
|
minimum: 0
|
||||||
default: 0
|
default: 0
|
||||||
|
BattleTurnParam:
|
||||||
|
name: turn
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: Turn number the battle was generated on.
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
BattleIDParam:
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: Battle identifier (RFC 4122 UUID).
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
schemas:
|
schemas:
|
||||||
HealthzResponse:
|
HealthzResponse:
|
||||||
type: object
|
type: object
|
||||||
@@ -788,6 +831,124 @@ components:
|
|||||||
wiped:
|
wiped:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: True when all population was eliminated by the bombing.
|
description: True when all population was eliminated by the bombing.
|
||||||
|
BattleReport:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
Full battle report. `races` and `ships` are JSON objects whose
|
||||||
|
keys are stringified integers used to cross-reference entries
|
||||||
|
from `protocol`: a `BattleActionReport` carries integer indices
|
||||||
|
into both maps. The serialised key is a string because JSON
|
||||||
|
object keys are always strings.
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- planet
|
||||||
|
- planetName
|
||||||
|
- races
|
||||||
|
- ships
|
||||||
|
- protocol
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: Battle identifier.
|
||||||
|
planet:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
description: Planet number the battle took place on.
|
||||||
|
planetName:
|
||||||
|
type: string
|
||||||
|
description: Planet name at battle start.
|
||||||
|
races:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
Participating races keyed by the integer index used in
|
||||||
|
`protocol.a` / `protocol.d`. Values are race identifiers.
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
ships:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
Participating ship groups keyed by the integer index used
|
||||||
|
in `protocol.sa` / `protocol.sd`.
|
||||||
|
additionalProperties:
|
||||||
|
$ref: "#/components/schemas/BattleReportGroup"
|
||||||
|
protocol:
|
||||||
|
type: array
|
||||||
|
description: Ordered list of shots exchanged during the battle.
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/BattleActionReport"
|
||||||
|
BattleReportGroup:
|
||||||
|
type: object
|
||||||
|
description: One ship group participating in the battle.
|
||||||
|
required:
|
||||||
|
- race
|
||||||
|
- className
|
||||||
|
- tech
|
||||||
|
- num
|
||||||
|
- numLeft
|
||||||
|
- loadType
|
||||||
|
- loadQuantity
|
||||||
|
- inBattle
|
||||||
|
properties:
|
||||||
|
race:
|
||||||
|
type: string
|
||||||
|
description: Race name of the group owner.
|
||||||
|
className:
|
||||||
|
type: string
|
||||||
|
description: Ship class name; resolvable through `LocalShipClass` or `OtherShipClass`.
|
||||||
|
tech:
|
||||||
|
type: object
|
||||||
|
description: Technology levels keyed by tech type name.
|
||||||
|
additionalProperties:
|
||||||
|
type: number
|
||||||
|
num:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
description: Initial number of ships in this group.
|
||||||
|
numLeft:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
description: Number of ships remaining at the end of the battle.
|
||||||
|
loadType:
|
||||||
|
type: string
|
||||||
|
description: Type of cargo loaded.
|
||||||
|
loadQuantity:
|
||||||
|
type: number
|
||||||
|
description: Quantity of cargo loaded.
|
||||||
|
inBattle:
|
||||||
|
type: boolean
|
||||||
|
description: |
|
||||||
|
True when the group actually fights. False groups observe
|
||||||
|
the battle in peace state and never fire or take damage.
|
||||||
|
BattleActionReport:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
One shot in the battle. Attacker and defender indices reference
|
||||||
|
`BattleReport.races`; ship-class indices reference
|
||||||
|
`BattleReport.ships`.
|
||||||
|
required:
|
||||||
|
- a
|
||||||
|
- sa
|
||||||
|
- d
|
||||||
|
- sd
|
||||||
|
- x
|
||||||
|
properties:
|
||||||
|
a:
|
||||||
|
type: integer
|
||||||
|
description: Index into `BattleReport.races` for the attacker.
|
||||||
|
sa:
|
||||||
|
type: integer
|
||||||
|
description: Index into `BattleReport.ships` for the attacker's group.
|
||||||
|
d:
|
||||||
|
type: integer
|
||||||
|
description: Index into `BattleReport.races` for the defender.
|
||||||
|
sd:
|
||||||
|
type: integer
|
||||||
|
description: Index into `BattleReport.ships` for the defender's group.
|
||||||
|
x:
|
||||||
|
type: boolean
|
||||||
|
description: True when the defender ship was destroyed by this shot.
|
||||||
IncomingGroup:
|
IncomingGroup:
|
||||||
type: object
|
type: object
|
||||||
description: An identified ship group inbound toward a planet of this race.
|
description: An identified ship group inbound toward a planet of this race.
|
||||||
|
|||||||
@@ -79,6 +79,13 @@ func TestGameOpenAPISpecFreezesResponseSchemas(t *testing.T) {
|
|||||||
status: http.StatusOK,
|
status: http.StatusOK,
|
||||||
wantRef: "#/components/schemas/HealthzResponse",
|
wantRef: "#/components/schemas/HealthzResponse",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "get battle",
|
||||||
|
path: "/api/v1/battle/{turn}/{uuid}",
|
||||||
|
method: http.MethodGet,
|
||||||
|
status: http.StatusOK,
|
||||||
|
wantRef: "#/components/schemas/BattleReport",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -271,6 +278,55 @@ func TestGameOpenAPISpecFreezesCommandRequest(t *testing.T) {
|
|||||||
require.Equal(t, uint64(1), cmdSchema.Value.MinItems, "CommandRequest.cmd minItems must be 1")
|
require.Equal(t, uint64(1), cmdSchema.Value.MinItems, "CommandRequest.cmd minItems must be 1")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGameOpenAPISpecFreezesGetBattleOperation(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
doc := loadOpenAPISpec(t)
|
||||||
|
operation := getOpenAPIOperation(t, doc, "/api/v1/battle/{turn}/{uuid}", http.MethodGet)
|
||||||
|
|
||||||
|
require.Equal(t, "getBattle", operation.OperationID, "GET /api/v1/battle/{turn}/{uuid} operation id")
|
||||||
|
|
||||||
|
paramRefs := make(map[string]bool)
|
||||||
|
for _, p := range operation.Parameters {
|
||||||
|
require.NotNil(t, p.Value, "parameter must have value")
|
||||||
|
paramRefs[p.Ref] = true
|
||||||
|
}
|
||||||
|
require.True(t, paramRefs["#/components/parameters/BattleTurnParam"], "GET /api/v1/battle/{turn}/{uuid} must reference BattleTurnParam")
|
||||||
|
require.True(t, paramRefs["#/components/parameters/BattleIDParam"], "GET /api/v1/battle/{turn}/{uuid} must reference BattleIDParam")
|
||||||
|
|
||||||
|
require.NotNil(t, operation.Responses, "operation must declare responses")
|
||||||
|
notFound := operation.Responses.Status(http.StatusNotFound)
|
||||||
|
require.NotNil(t, notFound, "operation must declare 404 response")
|
||||||
|
require.NotNil(t, notFound.Value, "404 response must have a value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGameOpenAPISpecFreezesBattleReport(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
doc := loadOpenAPISpec(t)
|
||||||
|
|
||||||
|
reportSchema := componentSchemaRef(t, doc, "BattleReport")
|
||||||
|
assertRequiredFields(t, reportSchema, "id", "planet", "planetName", "races", "ships", "protocol")
|
||||||
|
|
||||||
|
groupSchema := componentSchemaRef(t, doc, "BattleReportGroup")
|
||||||
|
assertRequiredFields(t, groupSchema, "race", "className", "tech", "num", "numLeft", "loadType", "loadQuantity", "inBattle")
|
||||||
|
|
||||||
|
actionSchema := componentSchemaRef(t, doc, "BattleActionReport")
|
||||||
|
assertRequiredFields(t, actionSchema, "a", "sa", "d", "sd", "x")
|
||||||
|
|
||||||
|
protocolSchema := reportSchema.Value.Properties["protocol"]
|
||||||
|
require.NotNil(t, protocolSchema, "BattleReport.protocol schema must exist")
|
||||||
|
require.True(t, protocolSchema.Value.Type.Is("array"), "BattleReport.protocol must be array")
|
||||||
|
require.NotNil(t, protocolSchema.Value.Items, "BattleReport.protocol items must be defined")
|
||||||
|
assertSchemaRef(t, protocolSchema.Value.Items, "#/components/schemas/BattleActionReport", "BattleReport.protocol items schema")
|
||||||
|
|
||||||
|
shipsSchema := reportSchema.Value.Properties["ships"]
|
||||||
|
require.NotNil(t, shipsSchema, "BattleReport.ships schema must exist")
|
||||||
|
require.True(t, shipsSchema.Value.Type.Is("object"), "BattleReport.ships must be object")
|
||||||
|
require.NotNil(t, shipsSchema.Value.AdditionalProperties.Schema, "BattleReport.ships additionalProperties must be a schema")
|
||||||
|
assertSchemaRef(t, shipsSchema.Value.AdditionalProperties.Schema, "#/components/schemas/BattleReportGroup", "BattleReport.ships additionalProperties schema")
|
||||||
|
}
|
||||||
|
|
||||||
func TestGameOpenAPISpecHealthzStatusEnum(t *testing.T) {
|
func TestGameOpenAPISpecHealthzStatusEnum(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user