From 1b0ab7a0793d5d391fa099a00efea98334d58913 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Wed, 7 Jan 2026 13:52:20 +0200 Subject: [PATCH] feat: http router, command api --- go.mod | 31 +++++++ go.sum | 77 ++++++++++++++++ internal/model/rest/command.go | 26 ++++++ internal/router/command.go | 49 ++++++++++ internal/router/middleware.go | 28 ++++++ internal/router/router.go | 40 ++++++++ internal/router/router_export_test.go | 10 ++ internal/router/router_test.go | 126 ++++++++++++++++++++++++++ internal/router/validator.go | 17 ++++ 9 files changed, 404 insertions(+) create mode 100644 internal/model/rest/command.go create mode 100644 internal/router/command.go create mode 100644 internal/router/middleware.go create mode 100644 internal/router/router.go create mode 100644 internal/router/router_export_test.go create mode 100644 internal/router/router_test.go create mode 100644 internal/router/validator.go diff --git a/go.mod b/go.mod index f766f9e..49ca40b 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,44 @@ module github.com/iliadenisov/galaxy go 1.25.0 require ( + github.com/gin-gonic/gin v1.11.0 + github.com/go-playground/validator/v10 v10.27.0 github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.11.1 golang.org/x/sys v0.36.0 ) require ( + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d73550d..114c923 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,91 @@ +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/model/rest/command.go b/internal/model/rest/command.go new file mode 100644 index 0000000..4ad7d2b --- /dev/null +++ b/internal/model/rest/command.go @@ -0,0 +1,26 @@ +package rest + +/* +Full list of requirements must be updated when adding new command: + +required_without_all=Vote DeclarePeace DeclareWar +| excluded_with=Vote DeclarePeace DeclareWar +*/ +type Command struct { + Race string `json:"race" binding:"required,notblank"` + Vote *CommandVote `json:"vote" binding:"required_without_all=DeclarePeace DeclareWar,excluded_with=DeclarePeace DeclareWar"` + DeclarePeace *CommandDeclarePeace `json:"declarePeace" binding:"required_without_all=Vote DeclareWar,excluded_with=Vote DeclareWar"` + DeclareWar *CommandDeclareWar `json:"declareWar" binding:"required_without_all=Vote DeclarePeace,excluded_with=Vote DeclarePeace"` +} + +type CommandVote struct { + Recipient string `json:"recipient" binding:"required,notblank"` +} + +type CommandDeclarePeace struct { + Opponent string `json:"recipient" binding:"required,notblank"` +} + +type CommandDeclareWar struct { + Opponent string `json:"recipient" binding:"required,notblank"` +} diff --git a/internal/router/command.go b/internal/router/command.go new file mode 100644 index 0000000..4a8ce4c --- /dev/null +++ b/internal/router/command.go @@ -0,0 +1,49 @@ +package router + +import ( + "errors" + "net/http" + + "github.com/iliadenisov/galaxy/internal/game" + + "github.com/gin-gonic/gin" + "github.com/iliadenisov/galaxy/internal/controller" + "github.com/iliadenisov/galaxy/internal/model/rest" +) + +var ( + ErrCommandNotProcessed = errors.New("command was not processed by executor") +) + +func CommandHandler(c *gin.Context, executor Executor) { + var cmd rest.Command + if err := c.ShouldBindJSON(&cmd); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + err := executor(cmd) + switch { + case err == nil: + c.Status(http.StatusOK) + case errors.Is(err, ErrCommandNotProcessed): + c.Status(http.StatusInternalServerError) + default: + // TODO: separate bad value errors and game state errors + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } +} + +func Execute(cmd rest.Command) error { + p := func(p *controller.Param) { + // TODO: initialize base controller settings + p.StoragePath = "" + } + switch { + case cmd.DeclareWar != nil: + return game.DeclareWar(p, cmd.Race, cmd.DeclareWar.Opponent) + case cmd.DeclarePeace != nil: + return game.DeclarePeace(p, cmd.Race, cmd.DeclareWar.Opponent) + default: + return ErrCommandNotProcessed + } +} diff --git a/internal/router/middleware.go b/internal/router/middleware.go new file mode 100644 index 0000000..dc6c66d --- /dev/null +++ b/internal/router/middleware.go @@ -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) + } + } +} diff --git a/internal/router/router.go b/internal/router/router.go new file mode 100644 index 0000000..5595b2f --- /dev/null +++ b/internal/router/router.go @@ -0,0 +1,40 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/go-playground/validator/v10" + "github.com/iliadenisov/galaxy/internal/model/rest" +) + +type Executor func(rest.Command) error + +type Router struct { + r *gin.Engine + executor Executor +} + +func (r Router) Run() error { + return r.r.Run() +} + +func NewRouter() Router { + return NewRouterExecutor(Execute) +} + +func NewRouterExecutor(executor Executor) Router { + return Router{r: setupRouter(executor)} +} + +func setupRouter(executor Executor) *gin.Engine { + gin.SetMode(gin.ReleaseMode) + r := gin.New() + r.Use(gin.Recovery()) + + if v, ok := binding.Validator.Engine().(*validator.Validate); ok { + v.RegisterValidation("notblank", notBlankStringValidator) + } + + r.PUT("/api/v1/command", LimitMiddleware(1), func(ctx *gin.Context) { CommandHandler(ctx, executor) }) + return r +} diff --git a/internal/router/router_export_test.go b/internal/router/router_export_test.go new file mode 100644 index 0000000..e386925 --- /dev/null +++ b/internal/router/router_export_test.go @@ -0,0 +1,10 @@ +package router + +import ( + "github.com/gin-gonic/gin" + "github.com/iliadenisov/galaxy/internal/model/rest" +) + +func SetupRouter() *gin.Engine { + return setupRouter(func(c rest.Command) error { return nil }) +} diff --git a/internal/router/router_test.go b/internal/router/router_test.go new file mode 100644 index 0000000..733301f --- /dev/null +++ b/internal/router/router_test.go @@ -0,0 +1,126 @@ +package router_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "sync" + "sync/atomic" + "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, 200, 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() + + 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 cmdBody(cmd rest.Command) *strings.Reader { + commandJson, _ := json.Marshal(cmd) + 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 +} diff --git a/internal/router/validator.go b/internal/router/validator.go new file mode 100644 index 0000000..ff66895 --- /dev/null +++ b/internal/router/validator.go @@ -0,0 +1,17 @@ +package router + +import ( + "strings" + + "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 +}