Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: separate validation, response and business logic #1301

Open
wants to merge 5 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/backend-integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
working-directory: backend
run:
go install github.com/swaggo/swag/cmd/swag@latest && swag init --ot json -o ./pkg/api/docs -d ./pkg/api/ -g ./handlers/public.go
go test -failfast ./pkg/api/... -config "${{ secrets.CI_CONFIG_PATH }}"
go test -failfast ./pkg/api/... -config "${{ secrets.CI_CONFIG_PATH }}" --tags=integration



2 changes: 1 addition & 1 deletion .github/workflows/backend-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
working-directory: ./backend
run: |
export TERM=xterm
go test `go list ./... | grep -v "pkg/consapi\|pkg/api\|internal/th\|internal/contracts\|pkg/commons/types\|pkg/commons/contracts\|cmd"` -coverprofile coverage.out -covermode atomic -race
go test `go list ./... | grep -v "pkg/consapi\|internal/th\|internal/contracts\|pkg/commons/types\|pkg/commons/contracts\|cmd"` -coverprofile coverage.out -covermode atomic -race

- name: coverage
working-directory: ./backend
Expand Down
2 changes: 1 addition & 1 deletion backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ require (
github.com/gobitfly/eth.store v0.0.0-20241115163631-57eeb551a84e
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang-jwt/jwt/v4 v4.5.1
github.com/golang/protobuf v1.5.4
github.com/gomodule/redigo v1.9.2
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.6.0
Expand Down Expand Up @@ -175,7 +176,6 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions backend/pkg/api/api_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//go:build integration
// +build integration

package api_test

import (
Expand Down
18 changes: 9 additions & 9 deletions backend/pkg/api/handlers/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ func (h *HandlerService) InternalPostUsers(w http.ResponseWriter, r *http.Reques
Email string `json:"email"`
Password string `json:"password"`
}{}
if err := v.checkBody(&req, r); err != nil {
if err := v.checkBody(&req, r.Body); err != nil {
handleErr(w, r, err)
return
}
Expand Down Expand Up @@ -321,7 +321,7 @@ func (h *HandlerService) InternalPostUserPasswordReset(w http.ResponseWriter, r
req := struct {
Email string `json:"email"`
}{}
if err := v.checkBody(&req, r); err != nil {
if err := v.checkBody(&req, r.Body); err != nil {
handleErr(w, r, err)
return
}
Expand Down Expand Up @@ -360,7 +360,7 @@ func (h *HandlerService) InternalPostUserPasswordResetHash(w http.ResponseWriter
req := struct {
Password string `json:"password"`
}{}
if err := v.checkBody(&req, r); err != nil {
if err := v.checkBody(&req, r.Body); err != nil {
handleErr(w, r, err)
return
}
Expand Down Expand Up @@ -428,7 +428,7 @@ func (h *HandlerService) InternalPostLogin(w http.ResponseWriter, r *http.Reques
Email string `json:"email"`
Password string `json:"password"`
}{}
if err := v.checkBody(&req, r); err != nil {
if err := v.checkBody(&req, r.Body); err != nil {
handleErr(w, r, err)
return
}
Expand Down Expand Up @@ -563,7 +563,7 @@ func (h *HandlerService) InternalPostMobileEquivalentExchange(w http.ResponseWri
RefreshToken string `json:"refresh_token"`
DeviceID string `json:"client_id"`
}{}
if err := v.checkBody(&req, r); err != nil {
if err := v.checkBody(&req, r.Body); err != nil {
handleErr(w, r, err)
return
}
Expand Down Expand Up @@ -629,7 +629,7 @@ func (h *HandlerService) InternalPostUsersMeNotificationSettingsPairedDevicesTok
req := struct {
Token string `json:"token"`
}{}
if err := v.checkBody(&req, r); err != nil {
if err := v.checkBody(&req, r.Body); err != nil {
handleErr(w, r, err)
return
}
Expand Down Expand Up @@ -658,7 +658,7 @@ const USER_SUBSCRIPTION_LIMIT = 8
func (h *HandlerService) InternalHandleMobilePurchase(w http.ResponseWriter, r *http.Request) {
var v validationError
req := types.MobileSubscription{}
if err := v.checkBody(&req, r); err != nil {
if err := v.checkBody(&req, r.Body); err != nil {
handleErr(w, r, err)
return
}
Expand Down Expand Up @@ -794,7 +794,7 @@ func (h *HandlerService) InternalPostUserEmail(w http.ResponseWriter, r *http.Re
Email string `json:"new_email"`
Password string `json:"password"`
}{}
if err := v.checkBody(&req, r); err != nil {
if err := v.checkBody(&req, r.Body); err != nil {
handleErr(w, r, err)
return
}
Expand Down Expand Up @@ -869,7 +869,7 @@ func (h *HandlerService) InternalPutUserPassword(w http.ResponseWriter, r *http.
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
}{}
if err := v.checkBody(&req, r); err != nil {
if err := v.checkBody(&req, r.Body); err != nil {
handleErr(w, r, err)
return
}
Expand Down
36 changes: 36 additions & 0 deletions backend/pkg/api/handlers/handler_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"strings"

"github.com/gobitfly/beaconchain/pkg/commons/log"
"github.com/gorilla/mux"
"github.com/invopop/jsonschema"

"github.com/alexedwards/scs/v2"
Expand Down Expand Up @@ -59,6 +60,41 @@ func (h *HandlerService) getDataAccessor(ctx context.Context) dataaccess.DataAcc
// all networks available in the system, filled on startup in NewHandlerService
var allNetworks []types.NetworkInfo

type Checker[T any] interface {
Check(params map[string]string, payload io.ReadCloser) (T, error)
}
Comment on lines +63 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo it's worth re-thinking naming for the interface and method name here, Check(er) is a bit vague. It's only supposed to be used in the context of input validation, right? Maybe InputValidator and Validate() or so


type BusinessLogicFunc[Input any, Response any] func(ctx context.Context, input Input) (Response, error)

func Handle[Input Checker[Input], Response any](defaultCode int, logicFunc BusinessLogicFunc[Input, Response]) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// prepare input
vars := mux.Vars(r)
q := r.URL.Query()
for k, v := range q {
if _, ok := vars[k]; ok || len(v) == 0 {
continue
}
vars[k] = v[0]
}
// input validation
var i Input
input, err := i.Check(vars, r.Body)
if err != nil {
handleErr(w, r, err)
return
}
// business logic
response, err := logicFunc(r.Context(), input)
if err != nil {
handleErr(w, r, err)
return
}

writeResponse(w, r, defaultCode, response)
}
}

// --------------------------------------
// errors

Expand Down
44 changes: 35 additions & 9 deletions backend/pkg/api/handlers/input_validation.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package handlers

import (
"bytes"
"cmp"
"encoding/base64"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -95,6 +95,13 @@ func (v *validationError) hasErrors() bool {
return v != nil && len(*v) > 0
}

func (v *validationError) AsError() error {
if v.hasErrors() {
return newBadRequestErr("%s", v.Error())
}
return nil
}

// --------------------------------------

func (v *validationError) checkRegex(regex *regexp.Regexp, param, paramName string) string {
Expand Down Expand Up @@ -142,14 +149,8 @@ func (v *validationError) checkUserEmailToken(token string) string {

// check request structure (body contains valid json and all required parameters are present)
// return error only if internal error occurs, otherwise add error to validationError and/or return nil
func (v *validationError) checkBody(data interface{}, r *http.Request) error {
// check if content type is application/json
if contentType := r.Header.Get("Content-Type"); !reJsonContentType.MatchString(contentType) {
v.add("request body", "'Content-Type' header must be 'application/json'")
}
Comment on lines -147 to -149
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this check be kept somehow? Maybe move it to middleware, like if there is a body present it should be application/json


bodyBytes, err := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewReader(bodyBytes)) // unconsume body for error logging
func (v *validationError) checkBody(data interface{}, requestBody io.ReadCloser) error { // <- should be checkBody after refactoring
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (v *validationError) checkBody(data interface{}, requestBody io.ReadCloser) error { // <- should be checkBody after refactoring
func (v *validationError) checkBody(data interface{}, requestBody io.ReadCloser) error {

bodyBytes, err := io.ReadAll(requestBody)
if err != nil {
return newInternalServerErr("error reading request body")
}
Expand Down Expand Up @@ -560,3 +561,28 @@ func (v *validationError) checkTimestamps(r *http.Request, chartLimits ChartTime
return afterTs, beforeTs
}
}

func (v *validationError) checkDashboardId(id string) interface{} {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks essentially like the same as https://github.com/gobitfly/beaconchain/blob/staging/backend/pkg/api/handlers/handler_service.go#L85-L109 to me, what's the reason for this? Would be cool if it was possible to merge them

if reInteger.MatchString(id) {
// given id is a normal id
id := v.checkUint(id, "dashboard_id")
return types.VDBIdPrimary(id)
}
if reValidatorDashboardPublicId.MatchString(id) {
// given id is a public id
return types.VDBIdPublic(id)
}
// given id must be an encoded set of validators
decodedId, err := base64.RawURLEncoding.DecodeString(id)
if err != nil {
v.add("dashboard_id", fmt.Sprintf("given value '%s' is not a valid dashboard id", id))
return nil
}
var validatorListError validationError
indexes, publicKeys := validatorListError.checkValidatorList(string(decodedId), forbidEmpty)
if validatorListError.hasErrors() {
v.add("dashboard_id", fmt.Sprintf("given value '%s' is not a valid dashboard id", id))
return nil
}
return validatorSet{Indexes: indexes, PublicKeys: publicKeys}
}
13 changes: 2 additions & 11 deletions backend/pkg/api/handlers/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func (h *HandlerService) InternalPostAdConfigurations(w http.ResponseWriter, r *
}

var req types.AdConfigurationData
if err := v.checkBody(&req, r); err != nil {
if err := v.checkBody(&req, r.Body); err != nil {
handleErr(w, r, err)
return
}
Expand Down Expand Up @@ -183,7 +183,7 @@ func (h *HandlerService) InternalPutAdConfiguration(w http.ResponseWriter, r *ht

key := v.checkKeyNotEmpty(mux.Vars(r)["key"])
var req types.AdConfigurationUpdateData
if err := v.checkBody(&req, r); err != nil {
if err := v.checkBody(&req, r.Body); err != nil {
handleErr(w, r, err)
return
}
Expand Down Expand Up @@ -348,10 +348,6 @@ func (h *HandlerService) InternalPutValidatorDashboardName(w http.ResponseWriter
h.PublicPutValidatorDashboardName(w, r)
}

func (h *HandlerService) InternalPostValidatorDashboardGroups(w http.ResponseWriter, r *http.Request) {
h.PublicPostValidatorDashboardGroups(w, r)
}

func (h *HandlerService) InternalPutValidatorDashboardGroups(w http.ResponseWriter, r *http.Request) {
h.PublicPutValidatorDashboardGroups(w, r)
}
Expand Down Expand Up @@ -431,11 +427,6 @@ func (h *HandlerService) InternalGetValidatorDashboardSlotViz(w http.ResponseWri
func (h *HandlerService) InternalGetValidatorDashboardSummary(w http.ResponseWriter, r *http.Request) {
h.PublicGetValidatorDashboardSummary(w, r)
}

func (h *HandlerService) InternalGetValidatorDashboardGroupSummary(w http.ResponseWriter, r *http.Request) {
h.PublicGetValidatorDashboardGroupSummary(w, r)
}

func (h *HandlerService) InternalGetValidatorDashboardSummaryChart(w http.ResponseWriter, r *http.Request) {
h.PublicGetValidatorDashboardSummaryChart(w, r)
}
Expand Down
Loading
Loading