diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 4192ee9..33b8b9a 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -35,12 +35,14 @@ type Param struct { StoragePath string } -func NewController(configure func(*Param)) (*Controller, error) { +type Config func(*Param) + +func NewController(config Config) (*Controller, error) { c := &Param{ StoragePath: ".", } - if configure != nil { - configure(c) + if config != nil { + config(c) } r, err := repo.NewFileRepo(c.StoragePath) if err != nil { diff --git a/internal/model/rest/init.go b/internal/model/rest/init.go new file mode 100644 index 0000000..3283fa7 --- /dev/null +++ b/internal/model/rest/init.go @@ -0,0 +1,15 @@ +package rest + +import "github.com/google/uuid" + +type Init struct { + Races []Race `json:"races" binding:"required,gte=10"` +} + +type Race struct { + Name string `json:"name" binding:"required,notblank"` +} + +type InitResponse struct { + UUID uuid.UUID `json:"uuid"` +} diff --git a/internal/router/command_test.go b/internal/router/command_test.go new file mode 100644 index 0000000..e19e9c3 --- /dev/null +++ b/internal/router/command_test.go @@ -0,0 +1,71 @@ +package router_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/iliadenisov/galaxy/internal/model/rest" + "github.com/iliadenisov/galaxy/internal/router" + "github.com/stretchr/testify/assert" +) + +func TestCommand(t *testing.T) { + r := router.SetupRouter() + + payload := rest.Command{ + Race: "SomeRace", + Vote: &rest.CommandVote{ + Recipient: "AnotherRace", + }, + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/api/v1/command", asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code, w.Body) + + // error: notblank validator + payload.Race = "" + w = httptest.NewRecorder() + req, _ = http.NewRequest("PUT", "/api/v1/command", asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) + + payload.Race = " " + w = httptest.NewRecorder() + req, _ = http.NewRequest("PUT", "/api/v1/command", asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) + + // error: no commands + payload = rest.Command{ + Race: "SomeRace", + } + + w = httptest.NewRecorder() + req, _ = http.NewRequest("PUT", "/api/v1/command", asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) + + // error: more than one command + payload = rest.Command{ + Race: "SomeRace", + Vote: &rest.CommandVote{ + Recipient: "AnotherRace", + }, + DeclarePeace: &rest.CommandDeclarePeace{ + Opponent: "OpponentRace", + }, + } + + w = httptest.NewRecorder() + req, _ = http.NewRequest("PUT", "/api/v1/command", asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) +} diff --git a/internal/router/command.go b/internal/router/handler/command.go similarity index 63% rename from internal/router/command.go rename to internal/router/handler/command.go index 83e83d5..3839a99 100644 --- a/internal/router/command.go +++ b/internal/router/handler/command.go @@ -1,26 +1,29 @@ -package router +package handler import ( "errors" "net/http" + "github.com/iliadenisov/galaxy/internal/controller" "github.com/iliadenisov/galaxy/internal/game" "github.com/gin-gonic/gin" "github.com/iliadenisov/galaxy/internal/model/rest" ) +type CommandExecutor func(controller.Config, rest.Command) error + var ( ErrCommandNotProcessed = errors.New("command was not processed by executor") ) -func CommandHandler(c *gin.Context, executor Executor) { +func CommandHandler(c *gin.Context, config controller.Config, executor CommandExecutor) { var cmd rest.Command if err := c.ShouldBindJSON(&cmd); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - err := executor(cmd) + err := executor(config, cmd) switch { case err == nil: c.Status(http.StatusOK) @@ -32,13 +35,12 @@ func CommandHandler(c *gin.Context, executor Executor) { } } -func Execute(cmd rest.Command) error { - p := param() +func ExecuteCommand(config controller.Config, cmd rest.Command) error { switch { case cmd.DeclareWar != nil: - return game.DeclareWar(p, cmd.Race, cmd.DeclareWar.Opponent) + return game.DeclareWar(config, cmd.Race, cmd.DeclareWar.Opponent) case cmd.DeclarePeace != nil: - return game.DeclarePeace(p, cmd.Race, cmd.DeclareWar.Opponent) + return game.DeclarePeace(config, cmd.Race, cmd.DeclareWar.Opponent) default: return ErrCommandNotProcessed } diff --git a/internal/router/status.go b/internal/router/handler/handler.go similarity index 52% rename from internal/router/status.go rename to internal/router/handler/handler.go index 3c5b25c..b6e69a5 100644 --- a/internal/router/status.go +++ b/internal/router/handler/handler.go @@ -1,4 +1,4 @@ -package router +package handler import ( "errors" @@ -6,19 +6,11 @@ import ( "github.com/gin-gonic/gin" e "github.com/iliadenisov/galaxy/internal/error" - "github.com/iliadenisov/galaxy/internal/game" - "github.com/iliadenisov/galaxy/internal/model/rest" ) -func StatusHandler(c *gin.Context) { - g, err := game.LoadState(param()) - +func transformError(c *gin.Context, err error) bool { if err == nil { - c.JSON(http.StatusOK, rest.Status{ - Turn: g.Age, - Players: len(g.Race), - }) - return + return false } var ge = new(e.GenericError) @@ -30,8 +22,10 @@ func StatusHandler(c *gin.Context) { default: c.JSON(http.StatusInternalServerError, gin.H{"generic_error": ge.Error(), "code": ge.Code}) } - return + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + } - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return true } diff --git a/internal/router/handler/init.go b/internal/router/handler/init.go new file mode 100644 index 0000000..12ce507 --- /dev/null +++ b/internal/router/handler/init.go @@ -0,0 +1,33 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/iliadenisov/galaxy/internal/controller" + "github.com/iliadenisov/galaxy/internal/game" + "github.com/iliadenisov/galaxy/internal/model/rest" +) + +func InitHandler(c *gin.Context, config controller.Config) { + var init rest.Init + if err := c.ShouldBindJSON(&init); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + races := make([]string, len(init.Races)) + for i := range init.Races { + races[i] = init.Races[i].Name + } + + uuid, err := game.GenerateGame(config, races) + if transformError(c, err) { + return + + } + + c.JSON(http.StatusCreated, rest.InitResponse{ + UUID: uuid, + }) +} diff --git a/internal/router/handler/status.go b/internal/router/handler/status.go new file mode 100644 index 0000000..8c439ca --- /dev/null +++ b/internal/router/handler/status.go @@ -0,0 +1,23 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/iliadenisov/galaxy/internal/controller" + "github.com/iliadenisov/galaxy/internal/game" + "github.com/iliadenisov/galaxy/internal/model/rest" +) + +func StatusHandler(c *gin.Context, config controller.Config) { + g, err := game.LoadState(config) + + if transformError(c, err) { + return + } + + c.JSON(http.StatusOK, rest.Status{ + Turn: g.Age, + Players: len(g.Race), + }) +} diff --git a/internal/router/init_test.go b/internal/router/init_test.go new file mode 100644 index 0000000..adb210c --- /dev/null +++ b/internal/router/init_test.go @@ -0,0 +1,55 @@ +package router_test + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/uuid" + "github.com/iliadenisov/galaxy/internal/controller" + "github.com/iliadenisov/galaxy/internal/model/rest" + "github.com/iliadenisov/galaxy/internal/router" + "github.com/iliadenisov/galaxy/internal/util" + "github.com/stretchr/testify/assert" +) + +func TestInit(t *testing.T) { + root, cleanup := util.CreateWorkDir(t) + defer cleanup() + + r := router.SetupRouterConfig(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.InitResponse + assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &initResponse)) + assert.NoError(t, uuid.Validate(initResponse.UUID.String())) +} + +func TestInitValidators(t *testing.T) { + r := router.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) +} + +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: fmt.Sprintf("Race_%02d", i)} + } + return request +} diff --git a/internal/router/router.go b/internal/router/router.go index 845ec61..01cf107 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -7,29 +7,19 @@ import ( "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/model/rest" + "github.com/iliadenisov/galaxy/internal/router/handler" ) -var ( - StoragePath string -) - -func init() { - StoragePath = os.Getenv("STORAGE_PATH") -} - -func param() func(*controller.Param) { +func initConfig() func(*controller.Param) { return func(p *controller.Param) { // TODO: initialize base controller settings - p.StoragePath = StoragePath + p.StoragePath = os.Getenv("STORAGE_PATH") } } -type Executor func(rest.Command) error - type Router struct { r *gin.Engine - executor Executor + executor handler.CommandExecutor } func (r Router) Run() error { @@ -37,14 +27,14 @@ func (r Router) Run() error { } func NewRouter() Router { - return NewRouterExecutor(Execute) + return NewRouterExecutor(handler.ExecuteCommand) } -func NewRouterExecutor(executor Executor) Router { - return Router{r: setupRouter(executor)} +func NewRouterExecutor(executor handler.CommandExecutor) Router { + return Router{r: setupRouter(initConfig(), executor)} } -func setupRouter(executor Executor) *gin.Engine { +func setupRouter(config controller.Config, executor handler.CommandExecutor) *gin.Engine { gin.SetMode(gin.ReleaseMode) r := gin.New() r.Use(gin.Recovery()) @@ -53,7 +43,11 @@ func setupRouter(executor Executor) *gin.Engine { v.RegisterValidation("notblank", notBlankStringValidator) } - r.GET("/api/v1/status", StatusHandler) - r.PUT("/api/v1/command", LimitMiddleware(1), func(ctx *gin.Context) { CommandHandler(ctx, executor) }) + groupV1 := r.Group("/api/v1") + + groupV1.GET("/status", func(ctx *gin.Context) { handler.StatusHandler(ctx, config) }) + groupV1.POST("/init", func(ctx *gin.Context) { handler.InitHandler(ctx, config) }) + groupV1.PUT("/command", LimitMiddleware(1), func(ctx *gin.Context) { handler.CommandHandler(ctx, config, executor) }) + return r } diff --git a/internal/router/router_export_test.go b/internal/router/router_export_test.go index e386925..540f469 100644 --- a/internal/router/router_export_test.go +++ b/internal/router/router_export_test.go @@ -2,9 +2,14 @@ package router import ( "github.com/gin-gonic/gin" + "github.com/iliadenisov/galaxy/internal/controller" "github.com/iliadenisov/galaxy/internal/model/rest" ) func SetupRouter() *gin.Engine { - return setupRouter(func(c rest.Command) error { return nil }) + return SetupRouterConfig(nil) +} + +func SetupRouterConfig(config controller.Config) *gin.Engine { + return setupRouter(config, func(controller.Config, rest.Command) error { return nil }) } diff --git a/internal/router/router_test.go b/internal/router/router_test.go index 204697a..4846f8f 100644 --- a/internal/router/router_test.go +++ b/internal/router/router_test.go @@ -10,71 +10,10 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/iliadenisov/galaxy/internal/model/rest" "github.com/iliadenisov/galaxy/internal/router" "github.com/stretchr/testify/assert" ) -func TestRouter(t *testing.T) { - r := router.SetupRouter() - - exampleCommand := rest.Command{ - Race: "SomeRace", - Vote: &rest.CommandVote{ - Recipient: "AnotherRace", - }, - } - - w := httptest.NewRecorder() - req, _ := http.NewRequest("PUT", "/api/v1/command", cmdBody(exampleCommand)) - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code, w.Body) - - // error: notblank validator - exampleCommand.Race = "" - w = httptest.NewRecorder() - req, _ = http.NewRequest("PUT", "/api/v1/command", cmdBody(exampleCommand)) - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) - - exampleCommand.Race = " " - w = httptest.NewRecorder() - req, _ = http.NewRequest("PUT", "/api/v1/command", cmdBody(exampleCommand)) - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) - - // error: no commands - exampleCommand = rest.Command{ - Race: "SomeRace", - } - - w = httptest.NewRecorder() - req, _ = http.NewRequest("PUT", "/api/v1/command", cmdBody(exampleCommand)) - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) - - // error: more than one command - exampleCommand = rest.Command{ - Race: "SomeRace", - Vote: &rest.CommandVote{ - Recipient: "AnotherRace", - }, - DeclarePeace: &rest.CommandDeclarePeace{ - Opponent: "OpponentRace", - }, - } - - w = httptest.NewRecorder() - req, _ = http.NewRequest("PUT", "/api/v1/command", cmdBody(exampleCommand)) - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusBadRequest, w.Code, w.Body) -} - func TestLimitConnections(t *testing.T) { r := limitTestingRouter() @@ -94,8 +33,8 @@ func TestLimitConnections(t *testing.T) { wg.Wait() } -func cmdBody(cmd rest.Command) *strings.Reader { - commandJson, _ := json.Marshal(cmd) +func asBody(body any) *strings.Reader { + commandJson, _ := json.Marshal(body) return strings.NewReader(string(commandJson)) } diff --git a/internal/router/status_test.go b/internal/router/status_test.go index 7f20d7d..0179b6c 100644 --- a/internal/router/status_test.go +++ b/internal/router/status_test.go @@ -17,5 +17,4 @@ func TestGetStatus(t *testing.T) { r.ServeHTTP(w, req) assert.Equal(t, http.StatusNotImplemented, w.Code, w.Body) - }