feat: server/version - validation improvements (#11)
ci/woodpecker/push/govulncheck Pipeline was successful Details
ci/woodpecker/push/test Pipeline was successful Details

Reviewed-on: twhelp/corev3#11
This commit is contained in:
Dawid Wysokiński 2024-03-01 06:33:48 +00:00
parent 572a85bc3c
commit c173bcdba6
10 changed files with 706 additions and 30 deletions

View File

@ -231,6 +231,10 @@ components:
description: Additional data related to the error. Can be used for i18n.
additionalProperties: true
x-go-type-skip-optional-pointer: true
VersionCode:
type: string
minLength: 2
maxLength: 2
Version:
type: object
required:
@ -240,8 +244,7 @@ components:
- timezone
properties:
code:
type: string
example: pl
$ref: "#/components/schemas/VersionCode"
host:
type: string
format: hostname
@ -252,6 +255,10 @@ components:
timezone:
type: string
example: Europe/Warsaw
ServerKey:
type: string
minLength: 1
maxLength: 10
Server:
type: object
required:
@ -267,8 +274,7 @@ components:
- createdAt
properties:
key:
type: string
example: pl151
$ref: "#/components/schemas/ServerKey"
open:
type: boolean
url:
@ -871,6 +877,9 @@ components:
$ref: "#/components/schemas/Building"
wood:
$ref: "#/components/schemas/Building"
IntId:
type: integer
minimum: 1
TribeOpponentsDefeated:
type: object
required:
@ -916,7 +925,7 @@ components:
- createdAt
properties:
id:
type: integer
$ref: "#/components/schemas/IntId"
name:
type: string
tag:
@ -1009,7 +1018,7 @@ components:
- createdAt
properties:
id:
type: integer
$ref: "#/components/schemas/IntId"
name:
type: string
rank:
@ -1175,27 +1184,25 @@ components:
name: versionCode
required: true
schema:
type: string
$ref: "#/components/schemas/VersionCode"
ServerKeyPathParam:
in: path
name: serverKey
required: true
schema:
type: string
$ref: "#/components/schemas/ServerKey"
TribeIdPathParam:
in: path
name: tribeId
required: true
schema:
type: integer
minimum: 1
$ref: "#/components/schemas/IntId"
PlayerIdPathParam:
in: path
name: playerId
required: true
schema:
type: integer
minimum: 1
$ref: "#/components/schemas/IntId"
responses:
ListVersionsResponse:
description: ""

View File

@ -8,11 +8,6 @@ import (
"time"
)
const (
serverKeyMinLength = 1
serverKeyMaxLength = 10
)
type Server struct {
key string
versionCode string
@ -358,7 +353,18 @@ func (params *UpdateServerParams) NumPlayers() NullInt {
}
func (params *UpdateServerParams) SetNumPlayers(numPlayers NullInt) error {
if numPlayers.Valid {
if err := validateIntInRange(numPlayers.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numPlayers",
Err: err,
}
}
}
params.numPlayers = numPlayers
return nil
}
@ -376,7 +382,18 @@ func (params *UpdateServerParams) NumVillages() NullInt {
}
func (params *UpdateServerParams) SetNumVillages(numVillages NullInt) error {
if numVillages.Valid {
if err := validateIntInRange(numVillages.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numVillages",
Err: err,
}
}
}
params.numVillages = numVillages
return nil
}
@ -385,7 +402,18 @@ func (params *UpdateServerParams) NumPlayerVillages() NullInt {
}
func (params *UpdateServerParams) SetNumPlayerVillages(numPlayerVillages NullInt) error {
if numPlayerVillages.Valid {
if err := validateIntInRange(numPlayerVillages.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numPlayerVillages",
Err: err,
}
}
}
params.numPlayerVillages = numPlayerVillages
return nil
}
@ -394,7 +422,18 @@ func (params *UpdateServerParams) NumBarbarianVillages() NullInt {
}
func (params *UpdateServerParams) SetNumBarbarianVillages(numBarbarianVillages NullInt) error {
if numBarbarianVillages.Valid {
if err := validateIntInRange(numBarbarianVillages.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numBarbarianVillages",
Err: err,
}
}
}
params.numBarbarianVillages = numBarbarianVillages
return nil
}
@ -403,7 +442,18 @@ func (params *UpdateServerParams) NumBonusVillages() NullInt {
}
func (params *UpdateServerParams) SetNumBonusVillages(numBonusVillages NullInt) error {
if numBonusVillages.Valid {
if err := validateIntInRange(numBonusVillages.V, 0, math.MaxInt); err != nil {
return ValidationError{
Model: updateServerParamsModelName,
Field: "numBonusVillages",
Err: err,
}
}
}
params.numBonusVillages = numBonusVillages
return nil
}
@ -453,6 +503,9 @@ func (params *UpdateServerParams) IsZero() bool {
!params.numPlayers.Valid &&
!params.playerDataSyncedAt.Valid &&
!params.numVillages.Valid &&
!params.numPlayerVillages.Valid &&
!params.numBarbarianVillages.Valid &&
!params.numBonusVillages.Valid &&
!params.villageDataSyncedAt.Valid &&
!params.ennoblementDataSyncedAt.Valid &&
!params.tribeSnapshotsCreatedAt.Valid &&
@ -567,7 +620,19 @@ func (params *ListServersParams) Keys() []string {
}
func (params *ListServersParams) SetKeys(keys []string) error {
for i, k := range keys {
if err := validateServerKey(k); err != nil {
return SliceElementValidationError{
Model: listServersParamsModelName,
Field: "keys",
Index: i,
Err: err,
}
}
}
params.keys = keys
return nil
}
@ -576,7 +641,19 @@ func (params *ListServersParams) VersionCodes() []string {
}
func (params *ListServersParams) SetVersionCodes(versionCodes []string) error {
for i, vc := range versionCodes {
if err := validateVersionCode(vc); err != nil {
return SliceElementValidationError{
Model: listServersParamsModelName,
Field: "versionCodes",
Index: i,
Err: err,
}
}
}
params.versionCodes = versionCodes
return nil
}

View File

@ -183,6 +183,321 @@ func TestUpdateServerParams_SetNumTribes(t *testing.T) {
}
}
func TestUpdateServerParams_SetNumPlayers(t *testing.T) {
t.Parallel()
type args struct {
numPlayers domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
numPlayers: domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "OK: null value",
args: args{
numPlayers: domain.NullInt{
Valid: false,
},
},
},
{
name: "ERR: numPlayers < 0",
args: args{
numPlayers: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numPlayers",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var params domain.UpdateServerParams
require.ErrorIs(t, params.SetNumPlayers(tt.args.numPlayers), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.numPlayers, params.NumPlayers())
})
}
}
func TestUpdateServerParams_SetNumVillages(t *testing.T) {
t.Parallel()
type args struct {
numVillages domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
numVillages: domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "OK: null value",
args: args{
numVillages: domain.NullInt{
Valid: false,
},
},
},
{
name: "ERR: numVillages < 0",
args: args{
numVillages: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numVillages",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var params domain.UpdateServerParams
require.ErrorIs(t, params.SetNumVillages(tt.args.numVillages), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.numVillages, params.NumVillages())
})
}
}
func TestUpdateServerParams_SetNumPlayerVillages(t *testing.T) {
t.Parallel()
type args struct {
numPlayerVillages domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
numPlayerVillages: domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "OK: null value",
args: args{
numPlayerVillages: domain.NullInt{
Valid: false,
},
},
},
{
name: "ERR: numPlayerVillages < 0",
args: args{
numPlayerVillages: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numPlayerVillages",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var params domain.UpdateServerParams
require.ErrorIs(t, params.SetNumPlayerVillages(tt.args.numPlayerVillages), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.numPlayerVillages, params.NumPlayerVillages())
})
}
}
func TestUpdateServerParams_SetNumBarbarianVillages(t *testing.T) {
t.Parallel()
type args struct {
numBarbarianVillages domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
numBarbarianVillages: domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "OK: null value",
args: args{
numBarbarianVillages: domain.NullInt{
Valid: false,
},
},
},
{
name: "ERR: numBarbarianVillages < 0",
args: args{
numBarbarianVillages: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numBarbarianVillages",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var params domain.UpdateServerParams
require.ErrorIs(t, params.SetNumBarbarianVillages(tt.args.numBarbarianVillages), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.numBarbarianVillages, params.NumBarbarianVillages())
})
}
}
func TestUpdateServerParams_SetNumBonusVillages(t *testing.T) {
t.Parallel()
type args struct {
numBonusVillages domain.NullInt
}
tests := []struct {
name string
args args
expectedErr error
}{
{
name: "OK",
args: args{
numBonusVillages: domain.NullInt{
V: gofakeit.IntRange(0, math.MaxInt),
Valid: true,
},
},
},
{
name: "OK: null value",
args: args{
numBonusVillages: domain.NullInt{
Valid: false,
},
},
},
{
name: "ERR: numBonusVillages < 0",
args: args{
numBonusVillages: domain.NullInt{
V: -1,
Valid: true,
},
},
expectedErr: domain.ValidationError{
Model: "UpdateServerParams",
Field: "numBonusVillages",
Err: domain.MinGreaterEqualError{
Min: 0,
Current: -1,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var params domain.UpdateServerParams
require.ErrorIs(t, params.SetNumBonusVillages(tt.args.numBonusVillages), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.numBonusVillages, params.NumBonusVillages())
})
}
}
func TestNewServerCursor(t *testing.T) {
t.Parallel()
@ -393,6 +708,114 @@ func TestListServersParams_SetEncodedCursor(t *testing.T) {
}
}
func TestListServersParams_SetKeys(t *testing.T) {
t.Parallel()
type args struct {
keys []string
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
keys: []string{
domaintest.RandServerKey(),
},
},
},
}
for _, serverKeyTest := range newServerKeyValidationTests() {
tests = append(tests, test{
name: serverKeyTest.name,
args: args{
keys: []string{serverKeyTest.key},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListServersParams",
Field: "keys",
Index: 0,
Err: serverKeyTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListServersParams()
require.ErrorIs(t, params.SetKeys(tt.args.keys), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.keys, params.Keys())
})
}
}
func TestListServersParams_SetVersionCodes(t *testing.T) {
t.Parallel()
type args struct {
versionCodes []string
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
versionCodes: []string{
domaintest.RandVersionCode(),
},
},
},
}
for _, versionCodeTest := range newVersionCodeValidationTests() {
tests = append(tests, test{
name: versionCodeTest.name,
args: args{
versionCodes: []string{versionCodeTest.code},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListServersParams",
Field: "versionCodes",
Index: 0,
Err: versionCodeTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListServersParams()
require.ErrorIs(t, params.SetVersionCodes(tt.args.versionCodes), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.versionCodes, params.VersionCodes())
})
}
}
func TestListServersParams_SetLimit(t *testing.T) {
t.Parallel()

View File

@ -296,10 +296,20 @@ func validateStringLen(s string, min, max int) error {
return nil
}
const (
versionCodeMinLength = 2
versionCodeMaxLength = 2
)
func validateVersionCode(code string) error {
return validateStringLen(code, versionCodeMinLength, versionCodeMaxLength)
}
const (
serverKeyMinLength = 1
serverKeyMaxLength = 10
)
func validateServerKey(key string) error {
return validateStringLen(key, serverKeyMinLength, serverKeyMaxLength)
}

View File

@ -5,11 +5,6 @@ import (
"net/url"
)
const (
versionCodeMinLength = 2
versionCodeMaxLength = 2
)
type Version struct {
code string
name string
@ -186,7 +181,19 @@ func (params *ListVersionsParams) Codes() []string {
}
func (params *ListVersionsParams) SetCodes(codes []string) error {
for i, c := range codes {
if err := validateVersionCode(c); err != nil {
return SliceElementValidationError{
Model: listVersionsParamsModelName,
Field: "codes",
Index: i,
Err: err,
}
}
}
params.codes = codes
return nil
}

View File

@ -65,6 +65,60 @@ func TestNewVersionCursor(t *testing.T) {
}
}
func TestListVersionsParams_SetCodes(t *testing.T) {
t.Parallel()
type args struct {
codes []string
}
type test struct {
name string
args args
expectedErr error
}
tests := []test{
{
name: "OK",
args: args{
codes: []string{
domaintest.RandVersionCode(),
},
},
},
}
for _, versionCodeTest := range newVersionCodeValidationTests() {
tests = append(tests, test{
name: versionCodeTest.name,
args: args{
codes: []string{versionCodeTest.code},
},
expectedErr: domain.SliceElementValidationError{
Model: "ListVersionsParams",
Field: "codes",
Index: 0,
Err: versionCodeTest.expectedErr,
},
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
params := domain.NewListVersionsParams()
require.ErrorIs(t, params.SetCodes(tt.args.codes), tt.expectedErr)
if tt.expectedErr != nil {
return
}
assert.Equal(t, tt.args.codes, params.Codes())
})
}
}
func TestListVersionsParams_SetSort(t *testing.T) {
t.Parallel()
@ -327,23 +381,25 @@ type versionCodeValidationTest struct {
}
func newVersionCodeValidationTests() []versionCodeValidationTest {
tooShort := gofakeit.LetterN(1)
tooLong := gofakeit.LetterN(3)
return []versionCodeValidationTest{
{
name: "ERR: version code length < 2",
code: "p",
code: tooShort,
expectedErr: domain.LenOutOfRangeError{
Min: 2,
Max: 2,
Current: len("p"),
Current: len(tooShort),
},
},
{
name: "ERR: version code length > 2",
code: "pll",
code: tooLong,
expectedErr: domain.LenOutOfRangeError{
Min: 2,
Max: 2,
Current: len("pll"),
Current: len(tooLong),
},
},
}

View File

@ -119,7 +119,7 @@ func (h *apiHTTPHandler) serverMiddleware(next http.Handler) http.Handler {
routeCtx.URLParams.Values[serverKeyIdx],
)
if err != nil {
h.errorRenderer.render(w, r, err)
h.errorRenderer.withErrorPathFormatter(formatGetServerErrorPath).render(w, r, err)
return
}
@ -150,8 +150,19 @@ func formatListServersErrorPath(segments []errorPathSegment) []string {
return []string{"$query", "limit"}
case "open":
return []string{"$query", "open"}
case "sort":
return []string{"$query", "sort"}
default:
return nil
}
}
func formatGetServerErrorPath(segments []errorPathSegment) []string {
if segments[0].model != "ListServersParams" {
return nil
}
switch segments[0].field {
case "keys":
return []string{"$path", "serverKey"}
default:
return nil
}

View File

@ -460,6 +460,42 @@ func TestGetServer(t *testing.T) {
assert.Equal(t, server.Server, body.Data)
},
},
{
name: "ERR: len(serverKey) > 10",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
req.URL.Path = fmt.Sprintf(endpointGetServer, server.Version.Code, gofakeit.LetterN(11))
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
pathSegments := strings.Split(req.URL.Path, "/")
require.Len(t, pathSegments, 6)
domainErr := domain.LenOutOfRangeError{
Min: 1,
Max: 10,
Current: len(pathSegments[5]),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: domainErr.Code(),
Message: domainErr.Error(),
Params: map[string]any{
"min": float64(domainErr.Min),
"max": float64(domainErr.Max),
"current": float64(domainErr.Current),
},
Path: []string{"$path", "serverKey"},
},
},
}, body)
},
},
{
name: "ERR: version not found",
reqModifier: func(t *testing.T, req *http.Request) {

View File

@ -54,7 +54,7 @@ func (h *apiHTTPHandler) versionMiddleware(next http.Handler) http.Handler {
version, err := h.versionSvc.Get(ctx, routeCtx.URLParams.Values[idx])
if err != nil {
h.errorRenderer.render(w, r, err)
h.errorRenderer.withErrorPathFormatter(formatGetVersionErrorPath).render(w, r, err)
return
}
@ -87,3 +87,16 @@ func formatListVersionsErrorPath(segments []errorPathSegment) []string {
return nil
}
}
func formatGetVersionErrorPath(segments []errorPathSegment) []string {
if segments[0].model != "ListVersionsParams" {
return nil
}
switch segments[0].field {
case "codes":
return []string{"$path", "versionCode"}
default:
return nil
}
}

View File

@ -344,6 +344,42 @@ func TestGetVersion(t *testing.T) {
assert.Equal(t, version, body.Data)
},
},
{
name: "ERR: len(versionCode) != 2",
reqModifier: func(t *testing.T, req *http.Request) {
t.Helper()
req.URL.Path = fmt.Sprintf(endpointGetVersion, domaintest.RandVersionCode()+domaintest.RandVersionCode())
},
assertResp: func(t *testing.T, req *http.Request, resp *http.Response) {
t.Helper()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
// body
body := decodeJSON[apimodel.ErrorResponse](t, resp.Body)
pathSegments := strings.Split(req.URL.Path, "/")
require.Len(t, pathSegments, 4)
domainErr := domain.LenOutOfRangeError{
Min: 2,
Max: 2,
Current: len(pathSegments[3]),
}
assert.Equal(t, apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Code: domainErr.Code(),
Message: domainErr.Error(),
Params: map[string]any{
"min": float64(domainErr.Min),
"max": float64(domainErr.Max),
"current": float64(domainErr.Current),
},
Path: []string{"$path", "versionCode"},
},
},
}, body)
},
},
{
name: "ERR: version not found",
reqModifier: func(t *testing.T, req *http.Request) {