Skip to content

Commit

Permalink
feat: Implement validation for HTTP FormValue(s) in frontend microser… (
Browse files Browse the repository at this point in the history
GoogleCloudPlatform#2438)

* feat: Implement validation for HTTP FormValue(s) in frontend microservice

* feat: Implement validation layer in frontend microservice

* Reduce loadgenerator "addToCart" quantity values

* Update ci-pr.yaml to test Go packages

---------

Co-authored-by: Nim Jayawardena <[email protected]>
Co-authored-by: Nim Jayawardena <[email protected]>
  • Loading branch information
3 people authored and azinneera committed Mar 29, 2024
1 parent 9ce39f3 commit 68bc066
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 22 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ jobs:
- name: Go Unit Tests
timeout-minutes: 10
run: |
for SERVICE in "shippingservice" "productcatalogservice"; do
echo "testing $SERVICE..."
pushd src/$SERVICE
for GO_PACKAGE in "shippingservice" "productcatalogservice" "frontend/validator"; do
echo "Testing $GO_PACKAGE..."
pushd src/$GO_PACKAGE
go test
popd
done
Expand Down
5 changes: 5 additions & 0 deletions src/frontend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,19 @@ require (
cloud.google.com/go/compute v1.23.3 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.19.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions src/frontend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,19 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
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.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
Expand Down Expand Up @@ -73,6 +81,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
63 changes: 45 additions & 18 deletions src/frontend/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (

pb "github.com/GoogleCloudPlatform/microservices-demo/src/frontend/genproto"
"github.com/GoogleCloudPlatform/microservices-demo/src/frontend/money"
"github.com/GoogleCloudPlatform/microservices-demo/src/frontend/validator"
)

type platformDetails struct {
Expand Down Expand Up @@ -224,19 +225,23 @@ func (fe *frontendServer) addToCartHandler(w http.ResponseWriter, r *http.Reques
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
quantity, _ := strconv.ParseUint(r.FormValue("quantity"), 10, 32)
productID := r.FormValue("product_id")
if productID == "" || quantity == 0 {
renderHTTPError(log, r, w, errors.New("invalid form input"), http.StatusBadRequest)
payload := validator.AddToCartPayload{
Quantity: quantity,
ProductID: productID,
}
if err := payload.Validate(); err != nil {
renderHTTPError(log, r, w, validator.ValidationErrorResponse(err), http.StatusUnprocessableEntity)
return
}
log.WithField("product", productID).WithField("quantity", quantity).Debug("adding to cart")
log.WithField("product", payload.ProductID).WithField("quantity", payload.Quantity).Debug("adding to cart")

p, err := fe.getProduct(r.Context(), productID)
p, err := fe.getProduct(r.Context(), payload.ProductID)
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "could not retrieve product"), http.StatusInternalServerError)
return
}

if err := fe.insertCart(r.Context(), sessionID(r), p.GetId(), int32(quantity)); err != nil {
if err := fe.insertCart(r.Context(), sessionID(r), p.GetId(), int32(payload.Quantity)); err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "failed to add to cart"), http.StatusInternalServerError)
return
}
Expand Down Expand Up @@ -350,22 +355,39 @@ func (fe *frontendServer) placeOrderHandler(w http.ResponseWriter, r *http.Reque
ccCVV, _ = strconv.ParseInt(r.FormValue("credit_card_cvv"), 10, 32)
)

payload := validator.PlaceOrderPayload{
Email: email,
StreetAddress: streetAddress,
ZipCode: zipCode,
City: city,
State: state,
Country: country,
CcNumber: ccNumber,
CcMonth: ccMonth,
CcYear: ccYear,
CcCVV: ccCVV,
}
if err := payload.Validate(); err != nil {
renderHTTPError(log, r, w, validator.ValidationErrorResponse(err), http.StatusUnprocessableEntity)
return
}

order, err := pb.NewCheckoutServiceClient(fe.checkoutSvcConn).
PlaceOrder(r.Context(), &pb.PlaceOrderRequest{
Email: email,
Email: payload.Email,
CreditCard: &pb.CreditCardInfo{
CreditCardNumber: ccNumber,
CreditCardExpirationMonth: int32(ccMonth),
CreditCardExpirationYear: int32(ccYear),
CreditCardCvv: int32(ccCVV)},
CreditCardNumber: payload.CcNumber,
CreditCardExpirationMonth: int32(payload.CcMonth),
CreditCardExpirationYear: int32(payload.CcYear),
CreditCardCvv: int32(payload.CcCVV)},
UserId: sessionID(r),
UserCurrency: currentCurrency(r),
Address: &pb.Address{
StreetAddress: streetAddress,
City: city,
State: state,
ZipCode: int32(zipCode),
Country: country},
StreetAddress: payload.StreetAddress,
City: payload.City,
State: payload.State,
ZipCode: int32(payload.ZipCode),
Country: payload.Country},
})
if err != nil {
renderHTTPError(log, r, w, errors.Wrap(err, "failed to complete the order"), http.StatusInternalServerError)
Expand Down Expand Up @@ -422,13 +444,18 @@ func (fe *frontendServer) logoutHandler(w http.ResponseWriter, r *http.Request)
func (fe *frontendServer) setCurrencyHandler(w http.ResponseWriter, r *http.Request) {
log := r.Context().Value(ctxKeyLog{}).(logrus.FieldLogger)
cur := r.FormValue("currency_code")
log.WithField("curr.new", cur).WithField("curr.old", currentCurrency(r)).
payload := validator.SetCurrencyPayload{Currency: cur}
if err := payload.Validate(); err != nil {
renderHTTPError(log, r, w, validator.ValidationErrorResponse(err), http.StatusUnprocessableEntity)
return
}
log.WithField("curr.new", payload.Currency).WithField("curr.old", currentCurrency(r)).
Debug("setting currency")

if cur != "" {
if payload.Currency != "" {
http.SetCookie(w, &http.Cookie{
Name: cookieCurrency,
Value: cur,
Value: payload.Currency,
MaxAge: cookieMaxAge,
})
}
Expand Down
83 changes: 83 additions & 0 deletions src/frontend/validator/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package validator

import (
"errors"
"fmt"

"github.com/go-playground/validator/v10"
)

var validate *validator.Validate

// init() is a special function that will run when this package is imported.
// It instantiates a SINGLE instance of *validator.Validate with the added
// benefit of caching struct info and validations.
func init() {
validate = validator.New(validator.WithRequiredStructEnabled())
}

type Payload interface {
Validate() error
}

type AddToCartPayload struct {
Quantity uint64 `validate:"required,gte=1,lte=10"`
ProductID string `validate:"required"`
}

type PlaceOrderPayload struct {
Email string `validate:"required,email"`
StreetAddress string `validate:"required,max=512"`
ZipCode int64 `validate:"required"`
City string `validate:"required,max=128"`
State string `validate:"required,max=128"`
Country string `validate:"required,max=128"`
CcNumber string `validate:"required,credit_card"`
CcMonth int64 `validate:"required,gte=1,lte=12"`
CcYear int64 `validate:"required"`
CcCVV int64 `validate:"required"`
}

type SetCurrencyPayload struct {
Currency string `validate:"required,iso4217"`
}

// Implementations of the 'Payload' interface.
func (ad *AddToCartPayload) Validate() error {
return validate.Struct(ad)
}

func (po *PlaceOrderPayload) Validate() error {
return validate.Struct(po)
}

func (sc *SetCurrencyPayload) Validate() error {
return validate.Struct(sc)
}

// Reusable error response function.
func ValidationErrorResponse(err error) error {
validationErrs, ok := err.(validator.ValidationErrors)
if !ok {
return errors.New("invalid validation error format")
}
var msg string
for _, err := range validationErrs {
msg += fmt.Sprintf("Field '%s' is invalid: %s\n", err.Field(), err.Tag())
}
return fmt.Errorf(msg)
}
Loading

0 comments on commit 68bc066

Please sign in to comment.