fs storage

This commit is contained in:
Ilia Denisov
2026-03-13 21:07:23 +02:00
committed by GitHub
parent 43039a79bf
commit 9ade76e21d
117 changed files with 1734 additions and 176 deletions
+942
View File
@@ -0,0 +1,942 @@
package router_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"galaxy/model/order"
"galaxy/model/rest"
"github.com/stretchr/testify/assert"
)
func TestCommandRaceQuit(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, apiCommandPath, 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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
payload.Actor = " "
w = httptest.NewRecorder()
req, _ = http.NewRequest(apiCommandMethod, apiCommandPath, 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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
// error: no commands
payload = &rest.Command{
Actor: commandDefaultActor,
}
w = httptest.NewRecorder()
req, _ = http.NewRequest(apiCommandMethod, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
}
func TestCommandRaceVote(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandRaceRelation(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandShipClassCreate(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandShipClassMerge(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandShipClassRemove(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandShipGroupBreak(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandShipGroupLoad(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandShipGroupUnload(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandShipGroupSend(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandShipGroupUpgrade(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandShipGroupMerge(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandShipGroupDismantle(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandShipGroupTransfer(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandShipGroupJoinFleet(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandFleetMerge(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandFleetSend(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandScienceCreate(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandScienceRemove(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandPlanetRename(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandPlanetProduce(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandPlanetRouteSet(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestCommandPlanetRouteRemove(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, tc.expectStatus, w.Code, w.Body)
})
}
}
func TestMultipleCommands(t *testing.T) {
e := newExecutor()
r := setupRouterExecutor(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, apiCommandPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, commandNoErrorsStatus, w.Code, w.Body)
assert.Equal(t, 2, e.(*dummyExecutor).CommandsExecuted)
}
+343
View File
@@ -0,0 +1,343 @@
package handler
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"galaxy/game/internal/controller"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"galaxy/model/order"
"galaxy/model/rest"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
func CommandHandler(c *gin.Context, executor CommandExecutor) {
var cmd rest.Command
if errorResponse(c, c.ShouldBindJSON(&cmd)) {
return
}
commands := make([]Command, len(cmd.Commands))
for i := range cmd.Commands {
command, err := parseCommand(cmd.Actor, cmd.Commands[i])
if errorResponse(c, err) {
return
}
commands[i] = command
}
if len(commands) == 0 {
errorResponse(c, errors.New("no commands given"))
return
}
if errorResponse(c, executor.Execute(commands...)) {
return
}
c.Status(http.StatusNoContent)
}
func parseCommand(actor string, c json.RawMessage) (Command, error) {
meta := new(order.CommandMeta)
if err := json.Unmarshal(c, meta); err != nil {
return nil, err
}
switch t := meta.CmdType; t {
case order.CommandTypeRaceQuit:
return commandRaceQuit(actor)
case order.CommandTypeRaceVote:
return commandRaceVote(actor, c)
case order.CommandTypeRaceRelation:
return commandRaceRelation(actor, c)
case order.CommandTypeShipClassCreate:
return commandShipClassCreate(actor, c)
case order.CommandTypeShipClassMerge:
return commandShipClassMerge(actor, c)
case order.CommandTypeShipClassRemove:
return commandShipClassRemove(actor, c)
case order.CommandTypeShipGroupBreak:
return commandShipGroupBreak(actor, c)
case order.CommandTypeShipGroupLoad:
return commandShipGroupLoad(actor, c)
case order.CommandTypeShipGroupUnload:
return commandShipGroupUnload(actor, c)
case order.CommandTypeShipGroupSend:
return commandShipGroupSend(actor, c)
case order.CommandTypeShipGroupUpgrade:
return commandShipGroupUpgrade(actor, c)
case order.CommandTypeShipGroupMerge:
return commandShipGroupMerge(actor, c)
case order.CommandTypeShipGroupDismantle:
return commandShipGroupDismantle(actor, c)
case order.CommandTypeShipGroupTransfer:
return commandShipGroupTransfer(actor, c)
case order.CommandTypeShipGroupJoinFleet:
return commandShipGroupJoinFleet(actor, c)
case order.CommandTypeFleetMerge:
return commandFleetMerge(actor, c)
case order.CommandTypeFleetSend:
return commandFleetSend(actor, c)
case order.CommandTypeScienceCreate:
return commandScienceCreate(actor, c)
case order.CommandTypeScienceRemove:
return commandScienceRemove(actor, c)
case order.CommandTypePlanetRename:
return commandPlanetRename(actor, c)
case order.CommandTypePlanetProduce:
return commandPlanetProduce(actor, c)
case order.CommandTypePlanetRouteSet:
return commandPlanetRouteSet(actor, c)
case order.CommandTypePlanetRouteRemove:
return commandPlanetRouteRemove(actor, c)
default:
return nil, fmt.Errorf("unknown comman type: %s", t)
}
}
func commandRaceQuit(actor string) (Command, error) {
return func(c controller.Ctrl) error { return c.RaceQuit(actor) }, nil
}
func commandRaceVote(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandRaceVote)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.RaceVote(actor, v.Acceptor)
}, nil
}
}
func commandRaceRelation(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandRaceRelation)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.RaceRelation(actor, v.Acceptor, v.Relation)
}, nil
}
}
func commandShipClassCreate(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandShipClassCreate)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.ShipClassCreate(actor, v.Name, v.Drive, int(v.Armament), v.Weapons, v.Shields, v.Cargo)
}, nil
}
}
func commandShipClassMerge(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandShipClassMerge)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.ShipClassMerge(actor, v.Name, v.Target)
}, nil
}
}
func commandShipClassRemove(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandShipClassRemove)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.ShipClassRemove(actor, v.Name)
}, nil
}
}
func commandShipGroupBreak(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandShipGroupBreak)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.ShipGroupBreak(actor, uuid.MustParse(v.ID), uuid.MustParse(v.NewID), uint(v.Quantity))
}, nil
}
}
func commandShipGroupLoad(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandShipGroupLoad)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.ShipGroupLoad(actor, uuid.MustParse(v.ID), v.Cargo, v.Quantity)
}, nil
}
}
func commandShipGroupUnload(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandShipGroupUnload)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.ShipGroupUnload(actor, uuid.MustParse(v.ID), v.Quantity)
}, nil
}
}
func commandShipGroupSend(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandShipGroupSend)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.ShipGroupSend(actor, uuid.MustParse(v.ID), uint(v.Destination))
}, nil
}
}
func commandShipGroupUpgrade(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandShipGroupUpgrade)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.ShipGroupUpgrade(actor, uuid.MustParse(v.ID), v.Tech, v.Level)
}, nil
}
}
func commandShipGroupMerge(actor string, c json.RawMessage) (Command, error) {
return func(c controller.Ctrl) error {
return c.ShipGroupMerge(actor)
}, nil
}
func commandShipGroupDismantle(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandShipGroupDismantle)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.ShipGroupDismantle(actor, uuid.MustParse(v.ID))
}, nil
}
}
func commandShipGroupTransfer(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandShipGroupTransfer)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.ShipGroupTransfer(actor, v.Acceptor, uuid.MustParse(v.ID))
}, nil
}
}
func commandShipGroupJoinFleet(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandShipGroupJoinFleet)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.ShipGroupJoinFleet(actor, v.Name, uuid.MustParse(v.ID))
}, nil
}
}
func commandFleetMerge(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandFleetMerge)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.FleetMerge(actor, v.Name, v.Target)
}, nil
}
}
func commandFleetSend(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandFleetSend)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.FleetSend(actor, v.Name, uint(v.Destination))
}, nil
}
}
func commandScienceCreate(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandScienceCreate)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.ScienceCreate(actor, v.Name, v.Drive, v.Weapons, v.Shields, v.Cargo)
}, nil
}
}
func commandScienceRemove(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandScienceRemove)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.ScienceRemove(actor, v.Name)
}, nil
}
}
func commandPlanetRename(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandPlanetRename)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.PlanetRename(actor, v.Number, v.Name)
}, nil
}
}
func commandPlanetProduce(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandPlanetProduce)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.PlanetProduce(actor, v.Number, v.Production, v.Subject)
}, nil
}
}
func commandPlanetRouteSet(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandPlanetRouteSet)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.PlanetRouteSet(actor, v.LoadType, uint(v.Origin), uint(v.Destination))
}, nil
}
}
func commandPlanetRouteRemove(actor string, c json.RawMessage) (Command, error) {
if v, err := unmarshallCommand(c, new(order.CommandPlanetRouteRemove)); err != nil {
return nil, err
} else {
return func(c controller.Ctrl) error {
return c.PlanetRouteRemove(actor, v.LoadType, uint(v.Origin))
}, nil
}
}
// Helpers
func unmarshallCommand[T order.DecodableCommand](c json.RawMessage, v T) (T, error) {
if err := json.Unmarshal(c, v); err != nil {
return v, err
}
if err := validateCommand(v); err != nil {
return v, err
}
return v, nil
}
func validateCommand(v order.DecodableCommand) error {
if ve, ok := binding.Validator.Engine().(*validator.Validate); ok {
if err := ve.Struct(v); err != nil {
return err
}
}
return nil
}
+124
View File
@@ -0,0 +1,124 @@
package handler
import (
"errors"
"net/http"
"os"
"galaxy/model/order"
"galaxy/model/rest"
e "galaxy/error"
"galaxy/game/internal/controller"
"galaxy/game/internal/model/game"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
type CommandExecutor interface {
GenerateGame([]string) (rest.StateResponse, error)
GenerateTurn() (rest.StateResponse, error)
GameState() (rest.StateResponse, error)
Execute(cmd ...Command) error
ValidateOrder(actor string, cmd ...order.DecodableCommand) error
}
type Command func(controller.Ctrl) error
type executor struct {
cfg controller.Configurer
}
func initConfig() controller.Configurer {
return func(p *controller.Param) {
p.StoragePath = os.Getenv("STORAGE_PATH")
}
}
func NewDefaultExecutor() CommandExecutor {
return NewDefaultConfigExecutor(initConfig())
}
func NewDefaultConfigExecutor(configurer controller.Configurer) CommandExecutor {
return &executor{cfg: configurer}
}
func (e *executor) Execute(cmd ...Command) error {
return controller.ExecuteCommand(e.cfg, func(c controller.Ctrl) error {
for i := range cmd {
if err := cmd[i](c); err != nil {
return err
}
}
return nil
})
}
func (e *executor) ValidateOrder(actor string, cmd ...order.DecodableCommand) error {
return controller.ValidateOrder(e.cfg, actor, cmd...)
}
func (e *executor) GenerateGame(races []string) (rest.StateResponse, error) {
s, err := controller.GenerateGame(e.cfg, races)
if err != nil {
return rest.StateResponse{}, err
}
return stateResponse(s), nil
}
func (e *executor) GenerateTurn() (rest.StateResponse, error) {
err := controller.GenerateTurn(e.cfg)
if err != nil {
return rest.StateResponse{}, err
}
return e.GameState()
}
func (e *executor) GameState() (rest.StateResponse, error) {
s, err := controller.GameState(e.cfg)
if err != nil {
return rest.StateResponse{}, err
}
return stateResponse(s), nil
}
func stateResponse(s game.State) rest.StateResponse {
result := &rest.StateResponse{
ID: s.ID,
Turn: s.Turn,
Stage: s.Stage,
Players: make([]rest.PlayerState, len(s.Players)),
}
for i := range s.Players {
result.Players[i].ID = s.Players[i].ID
result.Players[i].Name = s.Players[i].Name
result.Players[i].Extinct = s.Players[i].Extinct
}
return *result
}
func errorResponse(c *gin.Context, err error) bool {
if err == nil {
return false
}
if v, ok := err.(validator.ValidationErrors); ok {
c.JSON(http.StatusBadRequest, gin.H{"error": v.Error()})
return true
}
if ge, ok := errors.AsType[*e.GenericError](err); ok {
switch ge.Code {
case e.ErrGameNotInitialized:
c.Status(http.StatusNotImplemented)
default:
c.JSON(http.StatusInternalServerError, gin.H{"generic_error": ge.Error(), "code": ge.Code})
}
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
return true
}
+28
View File
@@ -0,0 +1,28 @@
package handler
import (
"net/http"
"galaxy/model/rest"
"github.com/gin-gonic/gin"
)
func InitHandler(c *gin.Context, executor CommandExecutor) {
var init rest.Init
if errorResponse(c, c.ShouldBindJSON(&init)) {
return
}
races := make([]string, len(init.Races))
for i := range init.Races {
races[i] = init.Races[i].Name
}
s, err := executor.GenerateGame(races)
if errorResponse(c, err) {
return
}
c.JSON(http.StatusCreated, s)
}
+39
View File
@@ -0,0 +1,39 @@
package handler
import (
"errors"
"net/http"
"galaxy/model/order"
"galaxy/model/rest"
"galaxy/game/internal/repo"
"github.com/gin-gonic/gin"
)
func OrderHandler(c *gin.Context, executor CommandExecutor) {
var cmd rest.Command
if errorResponse(c, c.ShouldBindJSON(&cmd)) {
return
}
commands := make([]order.DecodableCommand, len(cmd.Commands))
for i := range cmd.Commands {
command, err := repo.ParseOrder(cmd.Commands[i], validateCommand)
if errorResponse(c, err) {
return
}
commands[i] = command
}
if len(commands) == 0 {
errorResponse(c, errors.New("no commands given"))
return
}
if errorResponse(c, executor.ValidateOrder(cmd.Actor, commands...)) {
return
}
c.Status(http.StatusNoContent)
}
+17
View File
@@ -0,0 +1,17 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
)
func StatusHandler(c *gin.Context, executor CommandExecutor) {
state, err := executor.GameState()
if errorResponse(c, err) {
return
}
c.JSON(http.StatusOK, state)
}
+17
View File
@@ -0,0 +1,17 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
)
func TurnHandler(c *gin.Context, executor CommandExecutor) {
state, err := executor.GenerateTurn()
if errorResponse(c, err) {
return
}
c.JSON(http.StatusOK, state)
}
+49
View File
@@ -0,0 +1,49 @@
package router_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"galaxy/model/rest"
"galaxy/util"
"galaxy/game/internal/controller"
"galaxy/game/internal/router"
"galaxy/game/internal/router/handler"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestInit(t *testing.T) {
root, cleanup := util.CreateWorkDir(t)
defer cleanup()
r := router.SetupRouter(handler.NewDefaultConfigExecutor(func(p *controller.Param) { p.StoragePath = root }))
payload := generateInitRequest(10)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code, w.Body)
var initResponse rest.StateResponse
assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &initResponse))
assert.NoError(t, uuid.Validate(initResponse.ID.String()))
assert.NotEqual(t, uuid.Nil, uuid.MustParse(initResponse.ID.String()))
}
func TestInitValidators(t *testing.T) {
r := setupRouter()
payload := generateInitRequest(9)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
}
+28
View File
@@ -0,0 +1,28 @@
package router
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// LimitMiddleware limits number of concurrent connections using a buffered channel with limit spaces
func LimitMiddleware(limit int) gin.HandlerFunc {
if limit <= 0 {
panic("limit must be greater than 0")
}
semaphore := make(chan bool, limit)
t := time.NewTimer(time.Millisecond * 100)
return func(c *gin.Context) {
t.Reset(time.Millisecond * 100)
select {
case semaphore <- true:
c.Next()
<-semaphore
case <-t.C:
c.Status(http.StatusGatewayTimeout)
}
}
}
+942
View File
@@ -0,0 +1,942 @@
package router_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"galaxy/model/order"
"galaxy/model/rest"
"github.com/stretchr/testify/assert"
)
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)
// error: no commands
payload = &rest.Command{
Actor: commandDefaultActor,
}
w = httptest.NewRecorder()
req, _ = http.NewRequest(apiCommandMethod, apiOrderPath, asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
}
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 := newExecutor()
r := setupRouterExecutor(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.(*dummyExecutor).CommandsExecuted)
}
+92
View File
@@ -0,0 +1,92 @@
package router
import (
"fmt"
"io"
"net/http"
"os"
"galaxy/game/internal/router/handler"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
const (
ISO8601 = "2006-01-02 15:04:05.0 -07:00"
)
type Router struct {
r *gin.Engine
executor handler.CommandExecutor
}
func (r Router) Run() error {
return r.r.Run()
}
func NewRouter() Router {
gin.SetMode(gin.ReleaseMode)
return NewRouterExecutor(handler.NewDefaultExecutor())
}
func NewRouterExecutor(executor handler.CommandExecutor) Router {
return Router{r: setupRouter(executor)}
}
func setupRouter(executor handler.CommandExecutor) *gin.Engine {
r := gin.New()
// Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
logConfig := &gin.LoggerConfig{Formatter: logFormatter}
if gin.Mode() != gin.DebugMode {
logConfig.Output = io.Discard
}
r.Use(gin.LoggerWithConfig(*logConfig))
// Recovery middleware recovers from any panics and writes a 500 if there was one.
r.Use(gin.CustomRecovery(recoveryHandler))
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
if err := v.RegisterValidation("notblank", notBlankStringValidator); err != nil {
panic(err)
}
if err := v.RegisterValidation("ammoWeapons", armamentWithWeaponsValidator); err != nil {
panic(err)
}
if err := v.RegisterValidation("entity", entityNameStringValidator); err != nil {
panic(err)
}
if err := v.RegisterValidation("subject", productionTypeStringValidator); err != nil {
panic(err)
}
}
groupV1 := r.Group("/api/v1")
groupV1.GET("/status", func(ctx *gin.Context) { handler.StatusHandler(ctx, executor) })
groupV1.POST("/init", func(ctx *gin.Context) { handler.InitHandler(ctx, executor) })
groupV1.PUT("/command", LimitMiddleware(1), func(ctx *gin.Context) { handler.CommandHandler(ctx, executor) })
groupV1.PUT("/order", func(ctx *gin.Context) { handler.OrderHandler(ctx, executor) })
groupV1.PUT("/turn", func(ctx *gin.Context) { handler.TurnHandler(ctx, executor) })
return r
}
func logFormatter(param gin.LogFormatterParams) string {
return fmt.Sprintf("[%s] \"%s %s %s %d %s\"\n",
param.TimeStamp.Format(ISO8601),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
)
}
func recoveryHandler(c *gin.Context, recovered any) {
if err, ok := recovered.(string); ok {
fmt.Fprintf(os.Stderr, "recovered: %s", err)
}
c.AbortWithStatus(http.StatusInternalServerError)
}
@@ -0,0 +1,12 @@
package router
import (
"galaxy/game/internal/router/handler"
"github.com/gin-gonic/gin"
)
func SetupRouter(e handler.CommandExecutor) *gin.Engine {
gin.SetMode(gin.TestMode)
return setupRouter(e)
}
@@ -0,0 +1,76 @@
package router_test
import (
"encoding/json"
"net/http"
"galaxy/model/order"
"galaxy/model/rest"
"galaxy/game/internal/router"
"galaxy/game/internal/router/handler"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
var (
commandNoErrorsStatus = http.StatusNoContent
commandDefaultActor = "Gorlum"
apiCommandMethod = "PUT"
apiCommandPath = "/api/v1/command"
apiOrderPath = "/api/v1/order"
validId1 = id()
validId2 = id()
invalidId = "fd091c69-5976-4775-b2f9-7ba77735afb"
)
func id() string {
return uuid.New().String()
}
type dummyExecutor struct {
CommandsExecuted int
}
func (e *dummyExecutor) ValidateOrder(actor string, cmd ...order.DecodableCommand) error {
e.CommandsExecuted = len(cmd)
return nil
}
func (e *dummyExecutor) Execute(command ...handler.Command) error {
e.CommandsExecuted = len(command)
return nil
}
func (e *dummyExecutor) GenerateGame(races []string) (rest.StateResponse, error) {
return rest.StateResponse{}, nil
}
func (e *dummyExecutor) GenerateTurn() (rest.StateResponse, error) {
return rest.StateResponse{}, nil
}
func (e *dummyExecutor) GameState() (rest.StateResponse, error) {
return rest.StateResponse{}, nil
}
func setupRouter() *gin.Engine {
return setupRouterExecutor(newExecutor())
}
func setupRouterExecutor(e handler.CommandExecutor) *gin.Engine {
return router.SetupRouter(e)
}
func newExecutor() handler.CommandExecutor {
return &dummyExecutor{}
}
func encodeCommand(cmd any) json.RawMessage {
v, err := json.Marshal(cmd)
if err != nil {
panic(err)
}
return v
}
+83
View File
@@ -0,0 +1,83 @@
package router_test
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync"
"sync/atomic"
"testing"
"galaxy/model/rest"
"galaxy/game/internal/router"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestLimitConnections(t *testing.T) {
r := limitTestingRouter()
wg := sync.WaitGroup{}
lock := sync.WaitGroup{}
lock.Add(1)
for range 1000 {
wg.Go(func() {
w := httptest.NewRecorder()
lock.Wait()
req, _ := http.NewRequest("GET", "/limited", nil)
r.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code, w.Body)
})
}
lock.Done()
wg.Wait()
}
func asBody(body any) *strings.Reader {
commandJson, _ := json.Marshal(body)
return strings.NewReader(string(commandJson))
}
func limitTestingRouter() *gin.Engine {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Recovery())
counter := atomic.Int32{}
r.GET("/limited",
// limiting all ingoing connections
router.LimitMiddleware(1),
// storing counter value and testing increment after executing Next handlers
func(c *gin.Context) {
expected := counter.Load() + 1
c.Next()
current := counter.Load()
if current != expected {
c.String(http.StatusConflict, "expected: %d, got: %d", expected, current)
}
},
// increment counter
func(c *gin.Context) {
counter.Add(1)
c.Status(http.StatusOK)
})
return r
}
func generateInitRequest(races int) rest.Init {
request := rest.Init{
Races: make([]rest.Race, races),
}
for i := range request.Races {
request.Races[i] = rest.Race{Name: raceName(i)}
}
return request
}
func raceName(i int) string {
return fmt.Sprintf("Race_%02d", i)
}
+56
View File
@@ -0,0 +1,56 @@
package router_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"galaxy/model/rest"
"galaxy/util"
"galaxy/game/internal/controller"
"galaxy/game/internal/router"
"galaxy/game/internal/router/handler"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestGetStatus(t *testing.T) {
root, cleanup := util.CreateWorkDir(t)
defer cleanup()
r := router.SetupRouter(handler.NewDefaultConfigExecutor(func(p *controller.Param) { p.StoragePath = root }))
payload := generateInitRequest(10)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code, w.Body)
var initResponse rest.StateResponse
assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &initResponse))
assert.NoError(t, uuid.Validate(initResponse.ID.String()))
assert.NotEqual(t, uuid.Nil, uuid.MustParse(initResponse.ID.String()))
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/api/v1/status", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, w.Body)
var stateResponse rest.StateResponse
assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &stateResponse))
assert.NoError(t, uuid.Validate(stateResponse.ID.String()))
assert.Equal(t, initResponse.ID, stateResponse.ID)
assert.Equal(t, uint(0), stateResponse.Turn)
assert.Equal(t, uint(0), stateResponse.Stage)
assert.Len(t, stateResponse.Players, 10)
for i := range stateResponse.Players {
assert.NoError(t, uuid.Validate(stateResponse.Players[i].ID.String()))
assert.Equal(t, raceName(i), stateResponse.Players[i].Name)
assert.False(t, stateResponse.Players[i].Extinct)
}
}
+70
View File
@@ -0,0 +1,70 @@
package router_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"galaxy/model/rest"
"galaxy/util"
"galaxy/game/internal/controller"
"galaxy/game/internal/router"
"galaxy/game/internal/router/handler"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestGetTurn(t *testing.T) {
root, cleanup := util.CreateWorkDir(t)
defer cleanup()
r := router.SetupRouter(handler.NewDefaultConfigExecutor(func(p *controller.Param) { p.StoragePath = root }))
// create game
payload := generateInitRequest(10)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code, w.Body)
var initResponse rest.StateResponse
assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &initResponse))
assert.NoError(t, uuid.Validate(initResponse.ID.String()))
assert.NotEqual(t, uuid.Nil, uuid.MustParse(initResponse.ID.String()))
assert.Equal(t, uint(0), initResponse.Turn)
assert.Equal(t, uint(0), initResponse.Stage)
// generate next turn
w = httptest.NewRecorder()
req, _ = http.NewRequest("PUT", "/api/v1/turn", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, w.Body)
var turnResponse rest.StateResponse
assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &turnResponse))
assert.NoError(t, uuid.Validate(turnResponse.ID.String()))
assert.Equal(t, initResponse.ID, turnResponse.ID)
assert.Equal(t, uint(1), turnResponse.Turn)
assert.Equal(t, uint(0), turnResponse.Stage)
// validate status
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/api/v1/status", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, w.Body)
var stateResponse rest.StateResponse
assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &stateResponse))
assert.Equal(t, initResponse.ID, stateResponse.ID)
assert.Equal(t, uint(1), stateResponse.Turn)
assert.Equal(t, uint(0), stateResponse.Stage)
}
+66
View File
@@ -0,0 +1,66 @@
package router
import (
"strings"
"galaxy/util"
"github.com/go-playground/validator/v10"
)
var notBlankStringValidator validator.Func = func(fl validator.FieldLevel) bool {
s, ok := fl.Field().Interface().(string)
if ok {
if len(strings.TrimSpace(s)) == 0 {
return false
}
}
return true
}
var entityNameStringValidator validator.Func = func(fl validator.FieldLevel) bool {
s, ok := fl.Field().Interface().(string)
if ok {
if _, ok := util.ValidateTypeName(s); !ok {
return false
}
}
return true
}
var productionTypeStringValidator validator.Func = func(fl validator.FieldLevel) bool {
v, ok := fl.Field().Interface().(string)
if ok {
f := fl.Parent().FieldByName(fl.Param())
if f.String() == "SHIP" || f.String() == "SCIENCE" {
if _, ok := util.ValidateTypeName(v); !ok {
return false
}
}
}
return true
}
var armamentWithWeaponsValidator validator.Func = func(fl validator.FieldLevel) bool {
var v, compareTo float64
f := fl.Parent().FieldByName(fl.Param())
if f.CanFloat() {
compareTo = f.Float()
} else if f.CanInt() {
compareTo = float64(f.Int())
} else {
return false
}
if fl.Field().CanFloat() {
v = fl.Field().Float()
} else if fl.Field().CanInt() {
v = float64(fl.Field().Int())
} else {
return false
}
return (v == 0 && compareTo == 0) || (v >= 1 && compareTo >= 1)
}