diff --git a/cmd/pricer/main.go b/cmd/pricer/main.go index f26eec0..82968ad 100644 --- a/cmd/pricer/main.go +++ b/cmd/pricer/main.go @@ -19,7 +19,6 @@ import ( "github.com/mysteriumnetwork/discovery/middleware" "github.com/mysteriumnetwork/discovery/price" "github.com/mysteriumnetwork/discovery/price/pricingbyservice" - "github.com/mysteriumnetwork/go-rest/apierror" mlog "github.com/mysteriumnetwork/logger" ) @@ -41,8 +40,8 @@ func main() { r := gin.New() r.Use(gin.Recovery()) + r.Use(middleware.ErrorHandler) r.Use(middleware.Logger) - r.Use(apierror.ErrorHandler) rdb := redis.NewUniversalClient(&redis.UniversalOptions{ Addrs: cfg.RedisAddress, diff --git a/middleware/apierror.go b/middleware/apierror.go new file mode 100644 index 0000000..4ebe58d --- /dev/null +++ b/middleware/apierror.go @@ -0,0 +1,36 @@ +package middleware + +import ( + "encoding/json" + "errors" + + "github.com/gin-gonic/gin" + "github.com/mysteriumnetwork/go-rest/apierror" + "github.com/rs/zerolog/log" +) + +// ErrorHandler formats request errors unless the response has already been sent. +func ErrorHandler(c *gin.Context) { + c.Next() + if len(c.Errors) < 1 { + return + } + if c.Writer.Written() { + log.Err(c.Errors[0].Err).Msg("response already written, skipping error response") + return + } + + err := c.Errors[0].Err + var apiErr *apierror.APIError + if !errors.As(err, &apiErr) { + apiErr = apierror.Internal(err.Error(), apierror.ErrCodeInternal) + } + apiErr.Path = c.Request.URL.String() + + blob, err := json.Marshal(apiErr) + if err != nil { + c.Data(500, apierror.ContentTypeV1, apierror.DefaultErrStatic) + return + } + c.Data(apiErr.Status, apierror.ContentTypeV1, blob) +} diff --git a/price/api_by_service.go b/price/api_by_service.go index 2a92b53..6378565 100644 --- a/price/api_by_service.go +++ b/price/api_by_service.go @@ -2,6 +2,7 @@ package price import ( "context" + "encoding/json" "net/http" "time" @@ -15,6 +16,7 @@ import ( const ( errCodeParsingJson = "err_parsing_config" + errCodeMarshalJson = "err_marshal_prices" errCodeNoConfig = "err_no_config" errCodeUpdateConfig = "err_update_config" @@ -23,13 +25,17 @@ const ( ) type APIByService struct { - pricer *pricingbyservice.PriceGetter + pricer latestPricer cfger pricingbyservice.ConfigProvider redis redis.UniversalClient ac authCheck } +type latestPricer interface { + GetPrices() pricingbyservice.LatestPrices +} + type authCheck interface { JWTAuthorized() func(*gin.Context) } @@ -51,7 +57,14 @@ func NewAPIByService(redis redis.UniversalClient, pricer *pricingbyservice.Price // @Router /prices [get] // @Tags prices func (a *APIByService) LatestPrices(c *gin.Context) { - c.JSON(200, a.pricer.GetPrices()) + blob, err := json.Marshal(a.pricer.GetPrices()) + if err != nil { + log.Err(err).Msg("Failed to marshal latest prices") + c.Error(apierror.Internal(err.Error(), errCodeMarshalJson)) + return + } + + c.Data(http.StatusOK, gin.MIMEJSON, blob) } // GetConfig returns the base pricing config diff --git a/price/api_by_service_test.go b/price/api_by_service_test.go new file mode 100644 index 0000000..e5c20aa --- /dev/null +++ b/price/api_by_service_test.go @@ -0,0 +1,58 @@ +package price + +import ( + "math" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + + "github.com/mysteriumnetwork/discovery/middleware" + "github.com/mysteriumnetwork/discovery/price/pricingbyservice" +) + +type staticLatestPricer struct { + prices pricingbyservice.LatestPrices +} + +func (s staticLatestPricer) GetPrices() pricingbyservice.LatestPrices { + return s.prices +} + +func TestLatestPricesReturnsErrorWhenJSONMarshalFails(t *testing.T) { + gin.SetMode(gin.TestMode) + + api := &APIByService{ + pricer: staticLatestPricer{ + prices: pricingbyservice.LatestPrices{ + Defaults: &pricingbyservice.PriceHistory{ + Current: &pricingbyservice.PriceByType{ + Residential: &pricingbyservice.PriceByServiceType{ + Wireguard: pricingbyservice.Price{ + PricePerHourHumanReadable: math.NaN(), + }, + }, + }, + }, + }, + }, + } + + router := gin.New() + router.Use(middleware.ErrorHandler) + router.GET("/api/v4/prices", api.LatestPrices) + + req := httptest.NewRequest(http.MethodGet, "/api/v4/prices", nil) + resp := httptest.NewRecorder() + + router.ServeHTTP(resp, req) + + if resp.Code != http.StatusInternalServerError { + t.Fatalf("status = %d, want %d", resp.Code, http.StatusInternalServerError) + } + if !strings.Contains(resp.Body.String(), errCodeMarshalJson) { + t.Fatalf("response body = %q, want error code %q", resp.Body.String(), errCodeMarshalJson) + } +} diff --git a/price/pricingbyservice/price_updater_test.go b/price/pricingbyservice/price_updater_test.go index 12fd9bb..a5a007d 100644 --- a/price/pricingbyservice/price_updater_test.go +++ b/price/pricingbyservice/price_updater_test.go @@ -246,6 +246,18 @@ func TestPricer_isMystInSensibleLimit(t *testing.T) { }, wantErr: true, }, + { + name: "accepts price below ten cents when inside configured bound", + fields: fields{ + mystBound: Bound{ + Min: 0.01, + Max: 3, + }, + }, + args: args{ + price: 0.090547, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/sidecar/cmd/main.go b/sidecar/cmd/main.go index 0a9c96e..29887cb 100644 --- a/sidecar/cmd/main.go +++ b/sidecar/cmd/main.go @@ -77,7 +77,7 @@ func main() { mrkt, countryDemandIndexes, time.Minute*5, - pricingbyservice.Bound{Min: 0.1, Max: 3.0}, + pricingbyservice.Bound{Min: 0.01, Max: 3.0}, rdb, ) if err != nil {