diff --git a/api/openapi3.yml b/api/openapi3.yml index a6fe665..39d86c7 100644 --- a/api/openapi3.yml +++ b/api/openapi3.yml @@ -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: "" diff --git a/internal/domain/server.go b/internal/domain/server.go index dea6be2..457b462 100644 --- a/internal/domain/server.go +++ b/internal/domain/server.go @@ -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 } diff --git a/internal/domain/server_test.go b/internal/domain/server_test.go index ed624e5..50d6279 100644 --- a/internal/domain/server_test.go +++ b/internal/domain/server_test.go @@ -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() diff --git a/internal/domain/validation.go b/internal/domain/validation.go index caffbb9..37edfb5 100644 --- a/internal/domain/validation.go +++ b/internal/domain/validation.go @@ -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) } diff --git a/internal/domain/version.go b/internal/domain/version.go index 2b8a0b1..697d513 100644 --- a/internal/domain/version.go +++ b/internal/domain/version.go @@ -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 } diff --git a/internal/domain/version_test.go b/internal/domain/version_test.go index 99889ed..3fc701b 100644 --- a/internal/domain/version_test.go +++ b/internal/domain/version_test.go @@ -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), }, }, } diff --git a/internal/port/handler_http_api_server.go b/internal/port/handler_http_api_server.go index a86ff5f..a2a4a37 100644 --- a/internal/port/handler_http_api_server.go +++ b/internal/port/handler_http_api_server.go @@ -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 } diff --git a/internal/port/handler_http_api_server_test.go b/internal/port/handler_http_api_server_test.go index 95f8165..1f1beb6 100644 --- a/internal/port/handler_http_api_server_test.go +++ b/internal/port/handler_http_api_server_test.go @@ -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) { diff --git a/internal/port/handler_http_api_version.go b/internal/port/handler_http_api_version.go index e3bf42a..baa5193 100644 --- a/internal/port/handler_http_api_version.go +++ b/internal/port/handler_http_api_version.go @@ -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 + } +} diff --git a/internal/port/handler_http_api_version_test.go b/internal/port/handler_http_api_version_test.go index bdf1d44..1175439 100644 --- a/internal/port/handler_http_api_version_test.go +++ b/internal/port/handler_http_api_version_test.go @@ -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) {