From a442e488a799e01d807bdc4424759f70b62a48b8 Mon Sep 17 00:00:00 2001 From: clD11 <23483715+clD11@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:54:25 +0000 Subject: [PATCH] Add ZebPay linking metrics (#2235) * feat: add zebpay count linking metric * feat: add zebpay count linking metric * rename metrics * feat: add zebpay count linking metric * rename test var --- services/wallet/controllers_v3.go | 9 +- services/wallet/controllers_v3_test.go | 51 +++++-- services/wallet/controllers_v4_test.go | 26 ++-- services/wallet/keystore_test.go | 8 +- services/wallet/metric/metric.go | 44 ++++++ services/wallet/model/model.go | 6 + services/wallet/service.go | 120 ++++++++++------- services/wallet/service_test.go | 178 +++++++++++++++++++------ 8 files changed, 318 insertions(+), 124 deletions(-) create mode 100644 services/wallet/metric/metric.go diff --git a/services/wallet/controllers_v3.go b/services/wallet/controllers_v3.go index b3ec7df6b..a62da46d8 100644 --- a/services/wallet/controllers_v3.go +++ b/services/wallet/controllers_v3.go @@ -252,15 +252,14 @@ func LinkZebPayDepositAccountV3(s *Service) func(w http.ResponseWriter, r *http. }) } - xalr := &ZebPayLinkingRequest{} - if err := inputs.DecodeAndValidateReader(ctx, xalr, r.Body); err != nil { + zplReq := &ZebPayLinkingRequest{} + if err := inputs.DecodeAndValidateReader(ctx, zplReq, r.Body); err != nil { return HandleErrorsZebPay(err) } - country, err := s.LinkZebPayWallet(ctx, *id.UUID(), xalr.VerificationToken) + country, err := s.LinkZebPayWallet(ctx, *id.UUID(), zplReq.VerificationToken) if err != nil { - l.Error().Err(err).Str("paymentID", id.String()). - Msg("failed to link wallet") + l.Error().Err(err).Str("paymentID", id.String()).Msg("failed to link wallet") switch { case errors.Is(err, errorutils.ErrInvalidCountry): return handlers.WrapError(err, "region not supported", http.StatusBadRequest) diff --git a/services/wallet/controllers_v3_test.go b/services/wallet/controllers_v3_test.go index 890d583fc..aaf779622 100644 --- a/services/wallet/controllers_v3_test.go +++ b/services/wallet/controllers_v3_test.go @@ -214,7 +214,7 @@ func TestLinkBitFlyerWalletV3(t *testing.T) { }`, tokenString)), ) mockReputation = mockreputation.NewMockClient(mockCtrl) - s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil) + s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil, nil) handler = wallet.LinkBitFlyerDepositAccountV3(s) rw = httptest.NewRecorder() ) @@ -322,7 +322,7 @@ func TestLinkGeminiWalletV3RelinkBadRegion(t *testing.T) { "recipient_id": "%s" }`, linkingInfo, idTo)), ) - s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil) + s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil, nil) handler = wallet.LinkGeminiDepositAccountV3(s) rw = httptest.NewRecorder() ) @@ -427,7 +427,7 @@ func TestLinkGeminiWalletV3RelinkBadRegion(t *testing.T) { "DELETE", fmt.Sprintf("/v3/wallet/gemini/%s/claim", idFrom), nil) - s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil) + s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil, nil) handler = wallet.DisconnectCustodianLinkV3(s) rw = httptest.NewRecorder() @@ -552,7 +552,7 @@ func TestLinkGeminiWalletV3FirstLinking(t *testing.T) { "recipient_id": "%s" }`, linkingInfo, idTo)), ) - s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil) + s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil, nil) handler = wallet.LinkGeminiDepositAccountV3(s) rw = httptest.NewRecorder() ) @@ -670,7 +670,12 @@ func TestLinkZebPayWalletV3_InvalidKyc(t *testing.T) { }, }) - s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil) + mtc = &mockMtc{ + fnLinkFailureZP: func(cc string) { + assert.Equal(t, "IN", cc) + }, + } + s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil, mtc) handler = wallet.LinkZebPayDepositAccountV3(s) rw = httptest.NewRecorder() ) @@ -680,7 +685,11 @@ func TestLinkZebPayWalletV3_InvalidKyc(t *testing.T) { ctx = context.WithValue(ctx, appctx.ZebPayLinkingKeyCTXKey, base64.StdEncoding.EncodeToString(secret)) linkingInfo, err := jwt.Signed(sig).Claims(map[string]interface{}{ - "accountId": accountID, "depositId": idTo, "iat": time.Now().Unix(), "exp": time.Now().Add(5 * time.Second).Unix(), + "accountId": accountID, + "depositId": idTo, + "countryCode": "IN", + "iat": time.Now().Unix(), + "exp": time.Now().Add(5 * time.Second).Unix(), }).CompactSerialize() if err != nil { panic(err) @@ -742,7 +751,13 @@ func TestLinkZebPayWalletV3(t *testing.T) { // setup mock clients mockReputationClient = mockreputation.NewMockClient(mockCtrl) - s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil) + mtc = &mockMtc{ + fnLinkSuccessZP: func(cc string) { + assert.Equal(t, "IN", cc) + }, + } + + s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil, mtc) handler = wallet.LinkZebPayDepositAccountV3(s) rw = httptest.NewRecorder() ) @@ -867,7 +882,7 @@ func TestLinkGeminiWalletV3(t *testing.T) { "recipient_id": "%s" }`, linkingInfo, idTo)), ) - s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil) + s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil, nil) handler = wallet.LinkGeminiDepositAccountV3(s) rw = httptest.NewRecorder() ) @@ -971,8 +986,7 @@ func TestDisconnectCustodianLinkV3(t *testing.T) { r = httptest.NewRequest( "DELETE", fmt.Sprintf("/v3/wallet/gemini/%s/claim", idFrom), nil) - - s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil) + s, _ = wallet.InitService(datastore, nil, nil, nil, nil, nil, nil) handler = wallet.DisconnectCustodianLinkV3(s) w = httptest.NewRecorder() ) @@ -1028,3 +1042,20 @@ func mockSQLCustodianLink(mock sqlmock.Sqlmock, custodian string) { mock.ExpectQuery("^select(.+) from wallet_custodian(.+)"). WillReturnRows(clRow) } + +type mockMtc struct { + fnLinkSuccessZP func(cc string) + fnLinkFailureZP func(cc string) +} + +func (m *mockMtc) LinkSuccessZP(cc string) { + if m.fnLinkSuccessZP != nil { + m.fnLinkSuccessZP(cc) + } +} + +func (m *mockMtc) LinkFailureZP(cc string) { + if m.fnLinkFailureZP != nil { + m.fnLinkFailureZP(cc) + } +} diff --git a/services/wallet/controllers_v4_test.go b/services/wallet/controllers_v4_test.go index daabab8ff..42c1059d2 100644 --- a/services/wallet/controllers_v4_test.go +++ b/services/wallet/controllers_v4_test.go @@ -10,11 +10,12 @@ import ( "encoding/json" "errors" "fmt" - errorutils "github.com/brave-intl/bat-go/libs/errors" "net/http" "net/http/httptest" "testing" + errorutils "github.com/brave-intl/bat-go/libs/errors" + "github.com/brave-intl/bat-go/libs/clients" "github.com/brave-intl/bat-go/libs/altcurrency" @@ -72,7 +73,7 @@ func (suite *WalletControllersV4TestSuite) TestCreateBraveWalletV4_Success() { Validate(gomock.Any(), geoCountry). Return(true, nil) - service, err := wallet.InitService(storage, nil, reputationClient, nil, locationValidator, backoff.Retry) + service, err := wallet.InitService(storage, nil, reputationClient, nil, locationValidator, backoff.Retry, nil) suite.Require().NoError(err) router := chi.NewRouter() @@ -119,7 +120,7 @@ func (suite *WalletControllersV4TestSuite) TestCreateBraveWalletV4_GeoCountryDis Validate(gomock.Any(), gomock.Any()). Return(false, nil) - service, err := wallet.InitService(nil, nil, nil, nil, locationValidator, backoff.Retry) + service, err := wallet.InitService(nil, nil, nil, nil, locationValidator, backoff.Retry, nil) suite.Require().NoError(err) router := chi.NewRouter() @@ -171,7 +172,7 @@ func (suite *WalletControllersV4TestSuite) TestCreateBraveWalletV4_WalletAlready Validate(gomock.Any(), geoCountry). Return(true, nil) - service, err := wallet.InitService(storage, nil, nil, nil, locationValidator, nil) + service, err := wallet.InitService(storage, nil, nil, nil, locationValidator, nil, nil) suite.Require().NoError(err) router := chi.NewRouter() @@ -242,7 +243,7 @@ func (suite *WalletControllersV4TestSuite) TestCreateBraveWalletV4_ReputationCal Validate(gomock.Any(), gomock.Any()). Return(true, nil) - service, err := wallet.InitService(storage, nil, reputationClient, nil, locationValidator, backoff.Retry) + service, err := wallet.InitService(storage, nil, reputationClient, nil, locationValidator, backoff.Retry, nil) suite.Require().NoError(err) router := chi.NewRouter() @@ -292,8 +293,7 @@ func (suite *WalletControllersV4TestSuite) TestUpdateBraveWalletV4_Success() { UpsertReputationSummary(gomock.Any(), gomock.Any(), gomock.Any()). Return(nil) - service, err := wallet.InitService(storage, nil, reputationClient, nil, - nil, backoff.Retry) + service, err := wallet.InitService(storage, nil, reputationClient, nil, nil, backoff.Retry, nil) suite.Require().NoError(err) // create rewards wallet with public key @@ -343,8 +343,7 @@ func (suite *WalletControllersV4TestSuite) TestUpdateBraveWalletV4_VerificationM storage, err := wallet.NewWritablePostgres("", false, "") suite.NoError(err) - service, err := wallet.InitService(storage, nil, nil, nil, - nil, backoff.Retry) + service, err := wallet.InitService(storage, nil, nil, nil, nil, backoff.Retry, nil) suite.Require().NoError(err) publicKey, privateKey, err := httpsignature.GenerateEd25519Key(nil) @@ -382,8 +381,7 @@ func (suite *WalletControllersV4TestSuite) TestUpdateBraveWalletV4_PaymentIDMism storage, err := wallet.NewWritablePostgres("", false, "") suite.NoError(err) - service, err := wallet.InitService(storage, nil, nil, nil, - nil, backoff.Retry) + service, err := wallet.InitService(storage, nil, nil, nil, nil, backoff.Retry, nil) suite.Require().NoError(err) // create rewards wallet with public key @@ -450,8 +448,7 @@ func (suite *WalletControllersV4TestSuite) TestUpdateBraveWalletV4_GeoCountryAlr UpsertReputationSummary(gomock.Any(), gomock.Any(), gomock.Any()). Return(errorBundle) - service, err := wallet.InitService(storage, nil, reputationClient, nil, - nil, backoff.Retry) + service, err := wallet.InitService(storage, nil, reputationClient, nil, nil, backoff.Retry, nil) suite.Require().NoError(err) // create rewards wallet with public key @@ -516,8 +513,7 @@ func (suite *WalletControllersV4TestSuite) TestUpdateBraveWalletV4_ReputationCal UpsertReputationSummary(gomock.Any(), gomock.Any(), gomock.Any()). Return(errReputation) - service, err := wallet.InitService(storage, nil, reputationClient, nil, - nil, backoff.Retry) + service, err := wallet.InitService(storage, nil, reputationClient, nil, nil, backoff.Retry, nil) suite.Require().NoError(err) // create rewards wallet with public key diff --git a/services/wallet/keystore_test.go b/services/wallet/keystore_test.go index c0afb3c55..38f8c9052 100644 --- a/services/wallet/keystore_test.go +++ b/services/wallet/keystore_test.go @@ -103,7 +103,7 @@ func (suite *WalletControllersTestSuite) TestBalanceV3() { mockCtrl := gomock.NewController(suite.T()) defer mockCtrl.Finish() - service, _ := wallet.InitService(pg, nil, nil, nil, nil, nil) + service, _ := wallet.InitService(pg, nil, nil, nil, nil, nil, nil) w1 := suite.NewWallet(service, "uphold") @@ -163,7 +163,7 @@ func (suite *WalletControllersTestSuite) TestLinkWalletV3() { mockCtrl := gomock.NewController(suite.T()) defer mockCtrl.Finish() - service, _ := wallet.InitService(pg, nil, nil, nil, nil, nil) + service, _ := wallet.InitService(pg, nil, nil, nil, nil, nil, nil) w1 := suite.NewWallet(service, "uphold") w2 := suite.NewWallet(service, "uphold") @@ -319,7 +319,7 @@ func (suite *WalletControllersTestSuite) TestCreateBraveWalletV3() { pg, _, err := wallet.NewPostgres() suite.Require().NoError(err, "Failed to get postgres connection") - service, _ := wallet.InitService(pg, nil, nil, nil, nil, nil) + service, _ := wallet.InitService(pg, nil, nil, nil, nil, nil, nil) publicKey, privKey, err := httpsignature.GenerateEd25519Key(nil) @@ -362,7 +362,7 @@ func (suite *WalletControllersTestSuite) TestCreateUpholdWalletV3() { pg, _, err := wallet.NewPostgres() suite.Require().NoError(err, "Failed to get postgres connection") - service, _ := wallet.InitService(pg, nil, nil, nil, nil, nil) + service, _ := wallet.InitService(pg, nil, nil, nil, nil, nil, nil) publicKey, privKey, err := httpsignature.GenerateEd25519Key(nil) diff --git a/services/wallet/metric/metric.go b/services/wallet/metric/metric.go new file mode 100644 index 000000000..5cc520d38 --- /dev/null +++ b/services/wallet/metric/metric.go @@ -0,0 +1,44 @@ +package metric + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +const ( + status = "status" + countryCode = "country_code" +) + +type Metric struct { + cntLinkZP *prometheus.CounterVec +} + +// New returns a new metric.Metric. +// New panics if it cannot register any of the metrics. +func New() *Metric { + clzp := prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "count_link_zebpay", + Help: "Counts the number of successful and failed ZebPay linkings partitioned by country code", + }, + []string{status, countryCode}, + ) + prometheus.MustRegister(clzp) + + return &Metric{cntLinkZP: clzp} +} + +func (m *Metric) LinkSuccessZP(cc string) { + const success = "success" + m.cntLinkZP.With(prometheus.Labels{ + status: success, + countryCode: cc, + }).Inc() +} + +func (m *Metric) LinkFailureZP(cc string) { + const failure = "failure" + m.cntLinkZP.With(prometheus.Labels{ + status: failure, + countryCode: cc, + }).Inc() +} diff --git a/services/wallet/model/model.go b/services/wallet/model/model.go index 9f79ee3b1..e4220bd8c 100644 --- a/services/wallet/model/model.go +++ b/services/wallet/model/model.go @@ -3,3 +3,9 @@ package model import "errors" var ErrNoWalletCustodian = errors.New("model: no linked wallet custodian") + +type Error string + +func (e Error) Error() string { + return string(e) +} diff --git a/services/wallet/service.go b/services/wallet/service.go index 3c738e4a3..84954b3b2 100644 --- a/services/wallet/service.go +++ b/services/wallet/service.go @@ -14,10 +14,12 @@ import ( "sync" "time" + "github.com/brave-intl/bat-go/services/wallet/metric" "github.com/brave-intl/bat-go/services/wallet/model" "github.com/go-chi/chi" "github.com/go-jose/go-jose/v3/jwt" "github.com/lib/pq" + uuid "github.com/satori/go.uuid" "github.com/shopspring/decimal" "github.com/spf13/viper" @@ -103,6 +105,11 @@ type GeoValidator interface { Validate(ctx context.Context, geolocation string) (bool, error) } +type metricSvc interface { + LinkSuccessZP(cc string) + LinkFailureZP(cc string) +} + // Service contains datastore connections type Service struct { Datastore Datastore @@ -114,12 +121,11 @@ type Service struct { jobs []srv.Job crMu *sync.RWMutex custodianRegions custodian.Regions + metric metricSvc } // InitService creates a service using the passed datastore and clients configured from the environment -func InitService(datastore Datastore, roDatastore ReadOnlyDatastore, repClient reputation.Client, - geminiClient gemini.Client, geoCountryValidator GeoValidator, - retry backoff.RetryFunc) (*Service, error) { +func InitService(datastore Datastore, roDatastore ReadOnlyDatastore, repClient reputation.Client, geminiClient gemini.Client, geoCountryValidator GeoValidator, retry backoff.RetryFunc, metric metricSvc) (*Service, error) { service := &Service{ crMu: new(sync.RWMutex), Datastore: datastore, @@ -128,6 +134,7 @@ func InitService(datastore Datastore, roDatastore ReadOnlyDatastore, repClient r geminiClient: geminiClient, geoValidator: geoCountryValidator, retry: retry, + metric: metric, } // get the valid custodian regions return service, nil @@ -223,7 +230,9 @@ func SetupService(ctx context.Context) (context.Context, *Service) { geoCountryValidator := NewGeoCountryValidator(awsClient, config) - s, err := InitService(db, roDB, repClient, geminiClient, geoCountryValidator, backoff.Retry) + mtc := metric.New() + + s, err := InitService(db, roDB, repClient, geminiClient, geoCountryValidator, backoff.Retry, mtc) if err != nil { logger.Panic().Err(err).Msg("failed to initialize wallet service") } @@ -438,58 +447,22 @@ func (service *Service) LinkBitFlyerWallet(ctx context.Context, walletID uuid.UU // LinkZebPayWallet links a wallet and transfers funds to newly linked wallet. func (service *Service) LinkZebPayWallet(ctx context.Context, walletID uuid.UUID, verificationToken string) (string, error) { - const ( - depositProvider = "zebpay" - country = "IN" - ) + const depositProvider = "zebpay" - // Get zebpay linking_info signing key. - linkingKeyB64, ok := ctx.Value(appctx.ZebPayLinkingKeyCTXKey).(string) - if !ok { - const msg = "zebpay linking validation misconfigured" - return "", handlers.WrapError(appctx.ErrNotInContext, msg, http.StatusInternalServerError) - } - - // Decode base64 encoded jwt key. - decodedJWTKey, err := base64.StdEncoding.DecodeString(linkingKeyB64) - if err != nil { - const msg = "zebpay linking validation misconfigured" - return "", handlers.WrapError(appctx.ErrNotInContext, msg, http.StatusInternalServerError) - } - - // Parse the signed verification token from input. - tok, err := jwt.ParseSigned(verificationToken) + claims, err := parseZebPayClaims(ctx, verificationToken) if err != nil { - const msg = "zebpay linking info parsing failed" - return "", handlers.WrapError(appctx.ErrNotInContext, msg, http.StatusBadRequest) - } - - if len(tok.Headers) == 0 { - const msg = "linking info token invalid no headers" - return "", handlers.WrapError(errors.New(msg), msg, http.StatusBadRequest) - } - - // validate algorithm used - for i := range tok.Headers { - if tok.Headers[i].Algorithm != "HS256" { - const msg = "linking info token invalid" - return "", handlers.WrapError(errors.New(msg), msg, http.StatusBadRequest) - } - } - - // Create the jwt claims and get them (verified) from the token. - claims := &claimsZP{} - if err := tok.Claims(decodedJWTKey, claims); err != nil { - const msg = "zebpay linking info validation failed" - return "", handlers.WrapError(errors.New(msg), msg, http.StatusBadRequest) + return "", err } if err := claims.validate(time.Now()); err != nil { + service.metric.LinkFailureZP(claims.CountryCode) return "", err } err = validateCustodianLinking(ctx, service.Datastore, walletID, depositProvider) if err != nil { + service.metric.LinkFailureZP(claims.CountryCode) + if errors.Is(err, errCustodianLinkMismatch) { return "", errCustodianLinkMismatch } @@ -497,7 +470,9 @@ func (service *Service) LinkZebPayWallet(ctx context.Context, walletID uuid.UUID } providerLinkingID := uuid.NewV5(ClaimNamespace, claims.AccountID) - if err := service.Datastore.LinkWallet(ctx, walletID.String(), claims.DepositID, providerLinkingID, depositProvider, country); err != nil { + if err := service.Datastore.LinkWallet(ctx, walletID.String(), claims.DepositID, providerLinkingID, depositProvider, claims.CountryCode); err != nil { + service.metric.LinkFailureZP(claims.CountryCode) + if errors.Is(err, ErrUnusualActivity) { return "", handlers.WrapError(err, "unable to link - unusual activity", http.StatusBadRequest) } @@ -506,15 +481,16 @@ func (service *Service) LinkZebPayWallet(ctx context.Context, walletID uuid.UUID return "", handlers.WrapError(err, "mismatched provider account regions", http.StatusBadRequest) } - status := http.StatusInternalServerError if errors.Is(err, ErrTooManyCardsLinked) { - status = http.StatusConflict + return "", handlers.WrapError(err, "unable to link zebpay wallets", http.StatusConflict) } - return "", handlers.WrapError(err, "unable to link zebpay wallets", status) + return "", handlers.WrapError(err, "unable to link zebpay wallets", http.StatusInternalServerError) } - return country, nil + service.metric.LinkSuccessZP(claims.CountryCode) + + return claims.CountryCode, nil } // LinkGeminiWallet links a wallet and transfers funds to newly linked wallet @@ -907,3 +883,45 @@ func validateCustodianLinking(ctx context.Context, storage Datastore, walletID u return nil } + +const ( + errZPParseToken model.Error = "zebpay linking info parsing failed" + errZPNoHeaders model.Error = "linking info token invalid no headers" + errZPInvalidToken model.Error = "linking info token invalid" + errZPValidationFailed model.Error = "zebpay linking info validation failed" +) + +func parseZebPayClaims(ctx context.Context, verificationToken string) (claimsZP, error) { + const msgBadConf = "zebpay linking validation misconfigured" + linkingKeyB64, ok := ctx.Value(appctx.ZebPayLinkingKeyCTXKey).(string) + if !ok { + return claimsZP{}, handlers.WrapError(appctx.ErrNotInContext, msgBadConf, http.StatusInternalServerError) + } + + decodedJWTKey, err := base64.StdEncoding.DecodeString(linkingKeyB64) + if err != nil { + return claimsZP{}, handlers.WrapError(appctx.ErrNotInContext, msgBadConf, http.StatusInternalServerError) + } + + tok, err := jwt.ParseSigned(verificationToken) + if err != nil { + return claimsZP{}, handlers.WrapError(errZPParseToken, errZPParseToken.Error(), http.StatusBadRequest) + } + + if len(tok.Headers) == 0 { + return claimsZP{}, handlers.WrapError(errZPNoHeaders, errZPNoHeaders.Error(), http.StatusBadRequest) + } + + for i := range tok.Headers { + if tok.Headers[i].Algorithm != "HS256" { + return claimsZP{}, handlers.WrapError(errZPInvalidToken, errZPInvalidToken.Error(), http.StatusBadRequest) + } + } + + var claims claimsZP + if err := tok.Claims(decodedJWTKey, &claims); err != nil { + return claimsZP{}, handlers.WrapError(errZPValidationFailed, errZPValidationFailed.Error(), http.StatusBadRequest) + } + + return claims, nil +} diff --git a/services/wallet/service_test.go b/services/wallet/service_test.go index 3854f9787..7e6b3ef31 100644 --- a/services/wallet/service_test.go +++ b/services/wallet/service_test.go @@ -1,55 +1,21 @@ package wallet import ( + "context" + "encoding/base64" + "net/http" "testing" "time" + appctx "github.com/brave-intl/bat-go/libs/context" errorutils "github.com/brave-intl/bat-go/libs/errors" + "github.com/brave-intl/bat-go/libs/handlers" should "github.com/stretchr/testify/assert" must "github.com/stretchr/testify/require" "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/jwt" ) -func TestParseClaims(t *testing.T) { - secret := []byte("a jwt secret") - - sig, err := jose.NewSigner( - jose.SigningKey{Algorithm: jose.HS256, Key: secret}, - (&jose.SignerOptions{}).WithType("JWT"), - ) - must.Equal(t, nil, err) - - info, err := jwt.Signed(sig).Claims(map[string]interface{}{ - "accountId": "account_id", - "depositId": "deposit_id", - "iat": time.Date(2023, time.August, 16, 1, 1, 0, 0, time.UTC).Unix(), - "exp": time.Date(2023, time.August, 16, 1, 1, 2, 0, time.UTC).Unix(), - "isValid": true, - }).CompactSerialize() - - must.Equal(t, nil, err) - - tok, err := jwt.ParseSigned(info) - must.Equal(t, nil, err) - - actual := &claimsZP{} - { - err := tok.Claims(secret, actual) - must.Equal(t, nil, err) - } - - expected := &claimsZP{ - Iat: time.Date(2023, time.August, 16, 1, 1, 0, 0, time.UTC).Unix(), - Exp: time.Date(2023, time.August, 16, 1, 1, 2, 0, time.UTC).Unix(), - DepositID: "deposit_id", - AccountID: "account_id", - Valid: true, - } - - should.Equal(t, expected, actual) -} - func TestClaimsZP(t *testing.T) { type tcGiven struct { now time.Time @@ -209,3 +175,137 @@ func TestClaimsZP(t *testing.T) { }) } } + +func Test_parseZebPayClaims(t *testing.T) { + type tcGiven struct { + ctxKey appctx.CTXKey + secret string + sigAlgo string + zpLinkingKey string + claims map[string]interface{} + } + + type tcExpected struct { + claimsZP claimsZP + appErr error + } + + tests := []struct { + name string + given tcGiven + expected tcExpected + }{ + { + name: "success", + given: tcGiven{ + ctxKey: appctx.ZebPayLinkingKeyCTXKey, + secret: "test secret", + zpLinkingKey: base64.StdEncoding.EncodeToString([]byte("test secret")), + claims: map[string]interface{}{ + "iat": time.Date(2023, time.August, 16, 1, 1, 0, 0, time.UTC).Unix(), + "exp": time.Date(2023, time.August, 16, 1, 1, 2, 0, time.UTC).Unix(), + "depositId": "deposit_id", + "accountId": "account_id", + "isValid": true, + "countryCode": "", + }, + sigAlgo: "HS256", + }, + expected: tcExpected{ + claimsZP: claimsZP{ + Iat: time.Date(2023, time.August, 16, 1, 1, 0, 0, time.UTC).Unix(), + Exp: time.Date(2023, time.August, 16, 1, 1, 2, 0, time.UTC).Unix(), + DepositID: "deposit_id", + AccountID: "account_id", + Valid: true, + }, + }, + }, + { + name: "bad_config_key", + given: tcGiven{ + ctxKey: "bad_key", + sigAlgo: "HS256", + }, + expected: tcExpected{ + appErr: &handlers.AppError{ + Cause: appctx.ErrNotInContext, + Message: "zebpay linking validation misconfigured", + Code: http.StatusInternalServerError, + }, + }, + }, + { + name: "bad_config_decode", + given: tcGiven{ + ctxKey: appctx.ZebPayLinkingKeyCTXKey, + secret: "test secret", + sigAlgo: "HS256", + zpLinkingKey: "!!invalid_64!!", + }, + expected: tcExpected{ + appErr: &handlers.AppError{ + Cause: appctx.ErrNotInContext, + Message: "zebpay linking validation misconfigured", + Code: http.StatusInternalServerError, + }, + }, + }, + { + name: "wrong_signature_algorithm", + given: tcGiven{ + ctxKey: appctx.ZebPayLinkingKeyCTXKey, + secret: "test secret", + sigAlgo: "HS384", + zpLinkingKey: base64.StdEncoding.EncodeToString([]byte("test secret")), + }, + expected: tcExpected{ + appErr: &handlers.AppError{ + Cause: errZPInvalidToken, + Message: errZPInvalidToken.Error(), + Code: http.StatusBadRequest, + }, + }, + }, + { + name: "error_deserializing_claims", + given: tcGiven{ + ctxKey: appctx.ZebPayLinkingKeyCTXKey, + secret: "test secret", + sigAlgo: "HS256", + zpLinkingKey: base64.StdEncoding.EncodeToString([]byte("test secret")), + claims: map[string]interface{}{ + "accountId": 1, // invalid account type + }, + }, + expected: tcExpected{ + appErr: &handlers.AppError{ + Cause: errZPValidationFailed, + Message: errZPValidationFailed.Error(), + Code: http.StatusBadRequest, + }, + }, + }, + } + + for i := range tests { + tc := tests[i] + + t.Run(tc.name, func(t *testing.T) { + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(tc.given.sigAlgo), + Key: []byte(tc.given.secret), + }, (&jose.SignerOptions{}).WithType("JWT")) + must.Equal(t, nil, err) + + verificationToken, err := jwt.Signed(signer).Claims(tc.given.claims).CompactSerialize() + must.Equal(t, nil, err) + + ctx := context.WithValue(context.Background(), tc.given.ctxKey, tc.given.zpLinkingKey) + + actual, err := parseZebPayClaims(ctx, verificationToken) + should.Equal(t, tc.expected.claimsZP, actual) + should.Equal(t, tc.expected.appErr, err) + }) + } +}