From 1089634487fb58ee16db6b18abd74478f04307f0 Mon Sep 17 00:00:00 2001 From: husobee Date: Wed, 18 Oct 2023 13:28:42 -0400 Subject: [PATCH 1/6] validation of tlv2 time limited v2 blinded tokens submitted, fixes: #2157 --- services/skus/credentials.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/services/skus/credentials.go b/services/skus/credentials.go index 70f620bc1..03ec9c259 100644 --- a/services/skus/credentials.go +++ b/services/skus/credentials.go @@ -247,12 +247,28 @@ func (s *Service) CreateOrderItemCredentials(ctx context.Context, orderID uuid.U return errors.New("order item does not exist for order") } - if orderItem.CredentialType == "single-use" { + if orderItem.CredentialType == singleUse { if len(blindedCreds) > orderItem.Quantity { return errors.New("submitted more blinded creds than quantity of order item") } } + if orderItem.CredentialType == timeLimitedV2 { + // check order "numPerInterval" from metadata and multiply it by the buffer added to offset + numPerInterval, ok := order.Metadata["numPerInterval"].(int) + if !ok { + return errors.New("bad order: numPerInterval not set in order metadata") + } + numIntervals, ok := order.Metadata["numIntervals"].(int) + if !ok { + return errors.New("bad order: numIntervals not set in order metadata") + } + + if len(blindedCreds) > numPerInterval*numIntervals { + return errors.New("submitted more blinded creds than allowed for order") + } + } + issuerID, err := encodeIssuerID(order.MerchantID, orderItem.SKU) if err != nil { return errorutils.Wrap(err, "error encoding issuer name") From 6237ea8dcd96d400df4cf5a2192c31a287946b99 Mon Sep 17 00:00:00 2001 From: husobee Date: Wed, 18 Oct 2023 14:52:14 -0400 Subject: [PATCH 2/6] use default of 2 per interval credentials in this test --- services/skus/controllers_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/skus/controllers_test.go b/services/skus/controllers_test.go index 0942fa000..31e2b9530 100644 --- a/services/skus/controllers_test.go +++ b/services/skus/controllers_test.go @@ -1602,8 +1602,8 @@ func (suite *ControllersTestSuite) TestE2E_CreateOrderCreds_StoreSignedOrderCred } // This test performs a full e2e test using challenge bypass server to sign time limited v2 order credentials. -// It uses three tokens and expects three signing results (which is determined by the issuer buffer/overlap and CBR) -// which translates to three time limited v2 order credentials being stored for the single order containing +// It uses three tokens and expects two signing results (which is determined by the issuer buffer/overlap and CBR) +// which translates to two time limited v2 order credentials being stored for the single order containing // a single order item. func (suite *ControllersTestSuite) TestE2E_CreateOrderCreds_StoreSignedOrderCredentials_TimeLimitedV2() { ctx, cancel := context.WithCancel(context.Background()) @@ -1660,7 +1660,7 @@ func (suite *ControllersTestSuite) TestE2E_CreateOrderCreds_StoreSignedOrderCred ItemID: order.Items[0].ID, // these are already base64 encoded BlindedCreds: []string{"HLLrM7uBm4gVWr8Bsgx3M/yxDHVJX3gNow8Sx6sAPAY=", - "Hi1j/9Pen5vRvGSLn6eZCxgtkgZX7LU9edmOD2w5CWo=", "YG07TqExOSoo/46SIWK42OG0of3z94Y5SzCswW6sYSw="}, + "Hi1j/9Pen5vRvGSLn6eZCxgtkgZX7LU9edmOD2w5CWo="}, } payload, err := json.Marshal(data) @@ -1743,7 +1743,7 @@ func (suite *ControllersTestSuite) TestE2E_CreateOrderCreds_StoreSignedOrderCred suite.Assert().Equal(order.ID, response[0].OrderID) suite.Assert().NotEmpty(response[0].IssuerID) - suite.Assert().Equal(3, len(response)) + suite.Assert().Equal(2, len(response)) } func (suite *ControllersTestSuite) TestCreateOrderCreds_SingleUse_ExistingOrderCredentials() { From 40a315af5748664284739ec69de335ccd449a283 Mon Sep 17 00:00:00 2001 From: husobee Date: Thu, 19 Oct 2023 09:40:03 -0400 Subject: [PATCH 3/6] review feedback --- services/skus/credentials.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/services/skus/credentials.go b/services/skus/credentials.go index 03ec9c259..6d3ca17d3 100644 --- a/services/skus/credentials.go +++ b/services/skus/credentials.go @@ -219,6 +219,14 @@ type TimeLimitedCreds struct { Token string `json:"token"` } +var ( + numPerInterval = "numPerInterval" + numIntervals = "numIntervals" + errNumPerIntervalNotSet = errors.New("bad order: numPerInterval not set in order metadata") + errNumIntervalsNotSet = errors.New("bad order: numIntervals not set in order metadata") + errInvalidNumTokens = errors.New("submitted more blinded creds than allowed for order") +) + // CreateOrderItemCredentials creates the order credentials for the given order id using the supplied blinded credentials. // If the order is unpaid an error ErrOrderUnpaid is returned. func (s *Service) CreateOrderItemCredentials(ctx context.Context, orderID uuid.UUID, itemID uuid.UUID, blindedCreds []string) error { @@ -255,17 +263,17 @@ func (s *Service) CreateOrderItemCredentials(ctx context.Context, orderID uuid.U if orderItem.CredentialType == timeLimitedV2 { // check order "numPerInterval" from metadata and multiply it by the buffer added to offset - numPerInterval, ok := order.Metadata["numPerInterval"].(int) + numPerInterval, ok := order.Metadata[numPerInterval].(float64) if !ok { - return errors.New("bad order: numPerInterval not set in order metadata") + return errNumPerIntervalNotSet } - numIntervals, ok := order.Metadata["numIntervals"].(int) + numIntervals, ok := order.Metadata[numIntervals].(float64) if !ok { - return errors.New("bad order: numIntervals not set in order metadata") + return errNumIntervalsNotSet } - if len(blindedCreds) > numPerInterval*numIntervals { - return errors.New("submitted more blinded creds than allowed for order") + if len(blindedCreds) > int(numPerInterval*numIntervals) { + return errInvalidNumTokens } } From f8b0cd4a6fd9c438786671c126a4b13b7f69ba9d Mon Sep 17 00:00:00 2001 From: husobee Date: Thu, 19 Oct 2023 11:36:09 -0400 Subject: [PATCH 4/6] revert back the test to original --- services/skus/controllers_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/skus/controllers_test.go b/services/skus/controllers_test.go index 31e2b9530..0942fa000 100644 --- a/services/skus/controllers_test.go +++ b/services/skus/controllers_test.go @@ -1602,8 +1602,8 @@ func (suite *ControllersTestSuite) TestE2E_CreateOrderCreds_StoreSignedOrderCred } // This test performs a full e2e test using challenge bypass server to sign time limited v2 order credentials. -// It uses three tokens and expects two signing results (which is determined by the issuer buffer/overlap and CBR) -// which translates to two time limited v2 order credentials being stored for the single order containing +// It uses three tokens and expects three signing results (which is determined by the issuer buffer/overlap and CBR) +// which translates to three time limited v2 order credentials being stored for the single order containing // a single order item. func (suite *ControllersTestSuite) TestE2E_CreateOrderCreds_StoreSignedOrderCredentials_TimeLimitedV2() { ctx, cancel := context.WithCancel(context.Background()) @@ -1660,7 +1660,7 @@ func (suite *ControllersTestSuite) TestE2E_CreateOrderCreds_StoreSignedOrderCred ItemID: order.Items[0].ID, // these are already base64 encoded BlindedCreds: []string{"HLLrM7uBm4gVWr8Bsgx3M/yxDHVJX3gNow8Sx6sAPAY=", - "Hi1j/9Pen5vRvGSLn6eZCxgtkgZX7LU9edmOD2w5CWo="}, + "Hi1j/9Pen5vRvGSLn6eZCxgtkgZX7LU9edmOD2w5CWo=", "YG07TqExOSoo/46SIWK42OG0of3z94Y5SzCswW6sYSw="}, } payload, err := json.Marshal(data) @@ -1743,7 +1743,7 @@ func (suite *ControllersTestSuite) TestE2E_CreateOrderCreds_StoreSignedOrderCred suite.Assert().Equal(order.ID, response[0].OrderID) suite.Assert().NotEmpty(response[0].IssuerID) - suite.Assert().Equal(2, len(response)) + suite.Assert().Equal(3, len(response)) } func (suite *ControllersTestSuite) TestCreateOrderCreds_SingleUse_ExistingOrderCredentials() { From e03b5f1d72f46d2c461c7144864acdac77d94c6b Mon Sep 17 00:00:00 2001 From: husobee Date: Thu, 19 Oct 2023 11:51:33 -0400 Subject: [PATCH 5/6] adding test to excercise check --- services/skus/controllers_test.go | 90 +++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/services/skus/controllers_test.go b/services/skus/controllers_test.go index 0942fa000..4d227bd35 100644 --- a/services/skus/controllers_test.go +++ b/services/skus/controllers_test.go @@ -1601,6 +1601,96 @@ func (suite *ControllersTestSuite) TestE2E_CreateOrderCreds_StoreSignedOrderCred suite.Assert().NotEmpty(response[0].SignedCreds) } +func (suite *ControllersTestSuite) TestE2E_CreateOrderCreds_TooManyBlindedTokens_TimeLimitedV2() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + env := os.Getenv("ENV") + ctx = context.WithValue(ctx, appctx.EnvironmentCTXKey, env) + + // setup kafka + kafkaUnsignedOrderCredsTopic = os.Getenv("GRANT_CBP_SIGN_CONSUMER_TOPIC") + kafkaSignedOrderCredsDLQTopic = os.Getenv("GRANT_CBP_SIGN_CONSUMER_TOPIC_DLQ") + kafkaSignedOrderCredsTopic = os.Getenv("GRANT_CBP_SIGN_PRODUCER_TOPIC") + kafkaSignedRequestReaderGroupID = test.RandomString() + ctx = skustest.SetupKafka(ctx, suite.T(), kafkaUnsignedOrderCredsTopic, + kafkaSignedOrderCredsDLQTopic, kafkaSignedOrderCredsTopic) + + // create macaroon token for sku and whitelist + sku := test.RandomString() + price := 0 + token := suite.CreateMacaroon(sku, price) // create macaroon has a buffer of 3 hardcoded, no overlap + ctx = context.WithValue(ctx, appctx.WhitelistSKUsCTXKey, []string{token}) + + // create order with order items + request := model.CreateOrderRequest{ + Email: test.RandomString(), + Items: []model.OrderItemRequest{ + { + SKU: token, + Quantity: 1, + }, + }, + } + client, err := cbr.New() + suite.Require().NoError(err) + + retryPolicy = retrypolicy.NoRetry // set this so we fail fast + + service := &Service{ + issuerRepo: repository.NewIssuer(), + Datastore: suite.storage, + cbClient: client, + retry: backoff.Retry, + } + + order, err := service.CreateOrderFromRequest(ctx, request) + suite.Require().NoError(err) + + err = service.Datastore.UpdateOrder(order.ID, OrderStatusPaid) // to update the last paid at + suite.Require().NoError(err) + + // Create order credentials for the newly create order + data := CreateOrderCredsRequest{ + ItemID: order.Items[0].ID, + // these are already base64 encoded + BlindedCreds: []string{ // using 7 tokens should be too many for intervals 3, num per interval 2 + "HLLrM7uBm4gVWr8Bsgx3M/yxDHVJX3gNow8Sx6sAPAY=", + "HLLrM7uBm4gVWr8Bsgx3M/yxDHVJX3gNow8Sx6sAPAY=", + "HLLrM7uBm4gVWr8Bsgx3M/yxDHVJX3gNow8Sx6sAPAY=", + "HLLrM7uBm4gVWr8Bsgx3M/yxDHVJX3gNow8Sx6sAPAY=", + "HLLrM7uBm4gVWr8Bsgx3M/yxDHVJX3gNow8Sx6sAPAY=", + "Hi1j/9Pen5vRvGSLn6eZCxgtkgZX7LU9edmOD2w5CWo=", + "YG07TqExOSoo/46SIWK42OG0of3z94Y5SzCswW6sYSw="}, + } + + payload, err := json.Marshal(data) + suite.Require().NoError(err) + + requestID := uuid.NewV4().String() + ctx = context.WithValue(ctx, requestutils.RequestID, requestID) + + r := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/credentials", + order.ID), bytes.NewBuffer(payload)).WithContext(ctx) + + rw := httptest.NewRecorder() + + skuService, err := InitService(ctx, suite.storage, nil, repository.NewOrder(), repository.NewIssuer()) + suite.Require().NoError(err) + + authMwr := NewAuthMwr(skuService) + instrumentHandler := func(name string, h http.Handler) http.Handler { + return h + } + + router := Router(skuService, authMwr, instrumentHandler, newCORSOptsEnv()) + + server := &http.Server{Addr: ":8080", Handler: router} + server.Handler.ServeHTTP(rw, r) + + suite.Require().Equal(http.StatusBadRequest, rw.Code) // should get 400 back too many credentials +} + // This test performs a full e2e test using challenge bypass server to sign time limited v2 order credentials. // It uses three tokens and expects three signing results (which is determined by the issuer buffer/overlap and CBR) // which translates to three time limited v2 order credentials being stored for the single order containing From 53816434565b554911ce07d356a44098b1e82ab6 Mon Sep 17 00:00:00 2001 From: Pavel Brm <5097196+pavelbrm@users.noreply.github.com> Date: Sat, 21 Oct 2023 01:59:01 +1300 Subject: [PATCH 6/6] Patches For 2158 (#2160) * Refactor code and tests * Remove unnecessary type conversion * Use better names --- services/skus/controllers_test.go | 90 ---------- services/skus/credentials.go | 68 ++++---- services/skus/model/model.go | 55 ++++++ services/skus/service_nonint_test.go | 246 +++++++++++++++++++++++++++ 4 files changed, 340 insertions(+), 119 deletions(-) create mode 100644 services/skus/service_nonint_test.go diff --git a/services/skus/controllers_test.go b/services/skus/controllers_test.go index 4d227bd35..0942fa000 100644 --- a/services/skus/controllers_test.go +++ b/services/skus/controllers_test.go @@ -1601,96 +1601,6 @@ func (suite *ControllersTestSuite) TestE2E_CreateOrderCreds_StoreSignedOrderCred suite.Assert().NotEmpty(response[0].SignedCreds) } -func (suite *ControllersTestSuite) TestE2E_CreateOrderCreds_TooManyBlindedTokens_TimeLimitedV2() { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - env := os.Getenv("ENV") - ctx = context.WithValue(ctx, appctx.EnvironmentCTXKey, env) - - // setup kafka - kafkaUnsignedOrderCredsTopic = os.Getenv("GRANT_CBP_SIGN_CONSUMER_TOPIC") - kafkaSignedOrderCredsDLQTopic = os.Getenv("GRANT_CBP_SIGN_CONSUMER_TOPIC_DLQ") - kafkaSignedOrderCredsTopic = os.Getenv("GRANT_CBP_SIGN_PRODUCER_TOPIC") - kafkaSignedRequestReaderGroupID = test.RandomString() - ctx = skustest.SetupKafka(ctx, suite.T(), kafkaUnsignedOrderCredsTopic, - kafkaSignedOrderCredsDLQTopic, kafkaSignedOrderCredsTopic) - - // create macaroon token for sku and whitelist - sku := test.RandomString() - price := 0 - token := suite.CreateMacaroon(sku, price) // create macaroon has a buffer of 3 hardcoded, no overlap - ctx = context.WithValue(ctx, appctx.WhitelistSKUsCTXKey, []string{token}) - - // create order with order items - request := model.CreateOrderRequest{ - Email: test.RandomString(), - Items: []model.OrderItemRequest{ - { - SKU: token, - Quantity: 1, - }, - }, - } - client, err := cbr.New() - suite.Require().NoError(err) - - retryPolicy = retrypolicy.NoRetry // set this so we fail fast - - service := &Service{ - issuerRepo: repository.NewIssuer(), - Datastore: suite.storage, - cbClient: client, - retry: backoff.Retry, - } - - order, err := service.CreateOrderFromRequest(ctx, request) - suite.Require().NoError(err) - - err = service.Datastore.UpdateOrder(order.ID, OrderStatusPaid) // to update the last paid at - suite.Require().NoError(err) - - // Create order credentials for the newly create order - data := CreateOrderCredsRequest{ - ItemID: order.Items[0].ID, - // these are already base64 encoded - BlindedCreds: []string{ // using 7 tokens should be too many for intervals 3, num per interval 2 - "HLLrM7uBm4gVWr8Bsgx3M/yxDHVJX3gNow8Sx6sAPAY=", - "HLLrM7uBm4gVWr8Bsgx3M/yxDHVJX3gNow8Sx6sAPAY=", - "HLLrM7uBm4gVWr8Bsgx3M/yxDHVJX3gNow8Sx6sAPAY=", - "HLLrM7uBm4gVWr8Bsgx3M/yxDHVJX3gNow8Sx6sAPAY=", - "HLLrM7uBm4gVWr8Bsgx3M/yxDHVJX3gNow8Sx6sAPAY=", - "Hi1j/9Pen5vRvGSLn6eZCxgtkgZX7LU9edmOD2w5CWo=", - "YG07TqExOSoo/46SIWK42OG0of3z94Y5SzCswW6sYSw="}, - } - - payload, err := json.Marshal(data) - suite.Require().NoError(err) - - requestID := uuid.NewV4().String() - ctx = context.WithValue(ctx, requestutils.RequestID, requestID) - - r := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/%s/credentials", - order.ID), bytes.NewBuffer(payload)).WithContext(ctx) - - rw := httptest.NewRecorder() - - skuService, err := InitService(ctx, suite.storage, nil, repository.NewOrder(), repository.NewIssuer()) - suite.Require().NoError(err) - - authMwr := NewAuthMwr(skuService) - instrumentHandler := func(name string, h http.Handler) http.Handler { - return h - } - - router := Router(skuService, authMwr, instrumentHandler, newCORSOptsEnv()) - - server := &http.Server{Addr: ":8080", Handler: router} - server.Handler.ServeHTTP(rw, r) - - suite.Require().Equal(http.StatusBadRequest, rw.Code) // should get 400 back too many credentials -} - // This test performs a full e2e test using challenge bypass server to sign time limited v2 order credentials. // It uses three tokens and expects three signing results (which is determined by the issuer buffer/overlap and CBR) // which translates to three time limited v2 order credentials being stored for the single order containing diff --git a/services/skus/credentials.go b/services/skus/credentials.go index 6d3ca17d3..8158afd26 100644 --- a/services/skus/credentials.go +++ b/services/skus/credentials.go @@ -36,7 +36,9 @@ var ( ErrOrderUnpaid = errors.New("order not paid") ErrOrderHasNoItems = errors.New("order has no items") - errInvalidIssuerResp model.Error = "invalid issuer response" + errInvalidIssuerResp model.Error = "invalid issuer response" + errInvalidNCredsSingleUse model.Error = "submitted more blinded creds than quantity of order item" + errInvalidNCredsTlv2 model.Error = "submitted more blinded creds than allowed for order" defaultExpiresAt = time.Now().Add(17532 * time.Hour) // 2 years retryPolicy = retrypolicy.DefaultRetry @@ -219,14 +221,6 @@ type TimeLimitedCreds struct { Token string `json:"token"` } -var ( - numPerInterval = "numPerInterval" - numIntervals = "numIntervals" - errNumPerIntervalNotSet = errors.New("bad order: numPerInterval not set in order metadata") - errNumIntervalsNotSet = errors.New("bad order: numIntervals not set in order metadata") - errInvalidNumTokens = errors.New("submitted more blinded creds than allowed for order") -) - // CreateOrderItemCredentials creates the order credentials for the given order id using the supplied blinded credentials. // If the order is unpaid an error ErrOrderUnpaid is returned. func (s *Service) CreateOrderItemCredentials(ctx context.Context, orderID uuid.UUID, itemID uuid.UUID, blindedCreds []string) error { @@ -255,26 +249,8 @@ func (s *Service) CreateOrderItemCredentials(ctx context.Context, orderID uuid.U return errors.New("order item does not exist for order") } - if orderItem.CredentialType == singleUse { - if len(blindedCreds) > orderItem.Quantity { - return errors.New("submitted more blinded creds than quantity of order item") - } - } - - if orderItem.CredentialType == timeLimitedV2 { - // check order "numPerInterval" from metadata and multiply it by the buffer added to offset - numPerInterval, ok := order.Metadata[numPerInterval].(float64) - if !ok { - return errNumPerIntervalNotSet - } - numIntervals, ok := order.Metadata[numIntervals].(float64) - if !ok { - return errNumIntervalsNotSet - } - - if len(blindedCreds) > int(numPerInterval*numIntervals) { - return errInvalidNumTokens - } + if err := checkNumBlindedCreds(order, orderItem, len(blindedCreds)); err != nil { + return err } issuerID, err := encodeIssuerID(order.MerchantID, orderItem.SKU) @@ -673,3 +649,37 @@ func (s *Service) DeleteOrderCreds(ctx context.Context, orderID uuid.UUID, isSig return nil } + +// checkNumBlindedCreds checks the number of submitted blinded credentials. +// +// The number of submitted credentials must not exceed: +// - for single-use the quantity of the item; +// - for time-limited-v2 the product of numPerInterval and numIntervals. +func checkNumBlindedCreds(ord *model.Order, item *model.OrderItem, ncreds int) error { + switch item.CredentialType { + case singleUse: + if ncreds > item.Quantity { + return errInvalidNCredsSingleUse + } + + return nil + case timeLimitedV2: + nperInterval, err := ord.NumPerInterval() + if err != nil { + return err + } + + nintervals, err := ord.NumIntervals() + if err != nil { + return err + } + + if ncreds > nperInterval*nintervals { + return errInvalidNCredsTlv2 + } + + return nil + default: + return nil + } +} diff --git a/services/skus/model/model.go b/services/skus/model/model.go index d13859935..5d3c9e459 100644 --- a/services/skus/model/model.go +++ b/services/skus/model/model.go @@ -7,6 +7,7 @@ import ( "fmt" "net/url" "sort" + "strconv" "time" "github.com/lib/pq" @@ -32,10 +33,17 @@ const ( ErrInvalidOrderNoCancelURL Error = "model: invalid order: no cancel url" ErrInvalidOrderNoProductID Error = "model: invalid order: no product id" + ErrNumPerIntervalNotSet Error = "model: invalid order: numPerInterval must be set" + ErrNumIntervalsNotSet Error = "model: invalid order: numIntervals must be set" + ErrInvalidNumPerInterval Error = "model: invalid order: invalid numPerInterval" + ErrInvalidNumIntervals Error = "model: invalid order: invalid numIntervals" + // The text of the following errors is preserved as is, in case anything depends on them. ErrInvalidSKU Error = "Invalid SKU Token provided in request" ErrDifferentPaymentMethods Error = "all order items must have the same allowed payment methods" ErrInvalidOrderRequest Error = "model: no items to be created" + + errInvalidNumConversion Error = "model: invalid numeric conversion" ) const ( @@ -246,6 +254,34 @@ func (o *Order) GetTrialDays() int64 { return *o.TrialDays } +func (o *Order) NumPerInterval() (int, error) { + numRaw, ok := o.Metadata["numPerInterval"] + if !ok { + return 0, ErrNumPerIntervalNotSet + } + + result, err := numFromAny(numRaw) + if err != nil { + return 0, ErrInvalidNumPerInterval + } + + return result, nil +} + +func (o *Order) NumIntervals() (int, error) { + numRaw, ok := o.Metadata["numIntervals"] + if !ok { + return 0, ErrNumIntervalsNotSet + } + + result, err := numFromAny(numRaw) + if err != nil { + return 0, ErrInvalidNumIntervals + } + + return result, nil +} + // OrderItem represents a particular order item. type OrderItem struct { ID uuid.UUID `json:"id" db:"id"` @@ -568,3 +604,22 @@ func addURLParam(src, name, val string) (string, error) { return raw.String(), nil } + +func numFromAny(raw any) (int, error) { + switch v := raw.(type) { + case int: + return v, nil + case int64: + return int(v), nil + case int32: + return int(v), nil + case float32: + return int(v), nil + case float64: + return int(v), nil + case string: + return strconv.Atoi(v) + default: + return 0, errInvalidNumConversion + } +} diff --git a/services/skus/service_nonint_test.go b/services/skus/service_nonint_test.go new file mode 100644 index 000000000..1e192c8a7 --- /dev/null +++ b/services/skus/service_nonint_test.go @@ -0,0 +1,246 @@ +package skus + +import ( + "testing" + + uuid "github.com/satori/go.uuid" + should "github.com/stretchr/testify/assert" + + "github.com/brave-intl/bat-go/libs/datastore" + + "github.com/brave-intl/bat-go/services/skus/model" +) + +func TestCheckNumBlindedCreds(t *testing.T) { + type tcGiven struct { + ord *model.Order + item *model.OrderItem + ncreds int + } + + type testCase struct { + name string + given tcGiven + exp error + } + + tests := []testCase{ + { + name: "irrelevant_credential_type", + given: tcGiven{ + ord: &model.Order{ + ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + }, + item: &model.OrderItem{ + ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")), + OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + CredentialType: timeLimited, + }, + }, + }, + + { + name: "single_use_valid_1", + given: tcGiven{ + ord: &model.Order{ + ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + }, + item: &model.OrderItem{ + ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")), + OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + CredentialType: singleUse, + Quantity: 1, + }, + ncreds: 1, + }, + }, + + { + name: "single_use_valid_2", + given: tcGiven{ + ord: &model.Order{ + ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + }, + item: &model.OrderItem{ + ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")), + OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + CredentialType: singleUse, + Quantity: 2, + }, + ncreds: 1, + }, + }, + + { + name: "single_use_invalid", + given: tcGiven{ + ord: &model.Order{ + ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + }, + item: &model.OrderItem{ + ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")), + OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + CredentialType: singleUse, + Quantity: 2, + }, + ncreds: 3, + }, + exp: errInvalidNCredsSingleUse, + }, + + { + name: "tlv2_invalid_numPerInterval_missing", + given: tcGiven{ + ord: &model.Order{ + ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + Metadata: datastore.Metadata{}, + }, + item: &model.OrderItem{ + ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")), + OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + CredentialType: timeLimitedV2, + Quantity: 1, + }, + ncreds: 6, + }, + exp: model.ErrNumPerIntervalNotSet, + }, + + { + name: "tlv2_invalid_numPerInterval_invalid", + given: tcGiven{ + ord: &model.Order{ + ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + Metadata: datastore.Metadata{ + "numPerInterval": "NaN", + }, + }, + item: &model.OrderItem{ + ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")), + OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + CredentialType: timeLimitedV2, + Quantity: 1, + }, + ncreds: 6, + }, + exp: model.ErrInvalidNumPerInterval, + }, + + { + name: "tlv2_invalid_numIntervals_missing", + given: tcGiven{ + ord: &model.Order{ + ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + Metadata: datastore.Metadata{ + // We get a float64 upon fetching from the database. + "numPerInterval": float64(2), + }, + }, + item: &model.OrderItem{ + ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")), + OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + CredentialType: timeLimitedV2, + Quantity: 1, + }, + ncreds: 6, + }, + exp: model.ErrNumIntervalsNotSet, + }, + + { + name: "tlv2_invalid_numIntervals_invalid", + given: tcGiven{ + ord: &model.Order{ + ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + Metadata: datastore.Metadata{ + // We get a float64 upon fetching from the database. + "numPerInterval": float64(2), + "numIntervals": "NaN", + }, + }, + item: &model.OrderItem{ + ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")), + OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + CredentialType: timeLimitedV2, + Quantity: 1, + }, + ncreds: 6, + }, + exp: model.ErrInvalidNumIntervals, + }, + + { + name: "tlv2_valid_1", + given: tcGiven{ + ord: &model.Order{ + ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + Metadata: datastore.Metadata{ + // We get a float64 upon fetching from the database. + "numPerInterval": float64(2), + "numIntervals": float64(3), + }, + }, + item: &model.OrderItem{ + ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")), + OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + CredentialType: timeLimitedV2, + Quantity: 1, + }, + ncreds: 6, + }, + }, + + { + name: "tlv2_valid_2", + given: tcGiven{ + ord: &model.Order{ + ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + Metadata: datastore.Metadata{ + // We get a float64 upon fetching from the database. + "numPerInterval": float64(2), + "numIntervals": float64(4), + }, + }, + item: &model.OrderItem{ + ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")), + OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + CredentialType: timeLimitedV2, + Quantity: 1, + }, + ncreds: 6, + }, + }, + + { + name: "tlv2_invalid", + given: tcGiven{ + ord: &model.Order{ + ID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + Metadata: datastore.Metadata{ + // We get a float64 upon fetching from the database. + "numPerInterval": float64(2), + "numIntervals": float64(3), + }, + }, + item: &model.OrderItem{ + ID: uuid.Must(uuid.FromString("82514074-c4f5-4515-8d8d-29ab943615b3")), + OrderID: uuid.Must(uuid.FromString("df140c71-740b-46c9-bedd-27be0b1e6354")), + CredentialType: timeLimitedV2, + Quantity: 1, + }, + ncreds: 7, + }, + exp: errInvalidNCredsTlv2, + }, + } + + for i := range tests { + tc := tests[i] + + t.Run(tc.name, func(t *testing.T) { + actual := checkNumBlindedCreds(tc.given.ord, tc.given.item, tc.given.ncreds) + + should.Equal(t, tc.exp, actual) + }) + } +}