From 79438de820cea066183f99523a2197e8089b86bd Mon Sep 17 00:00:00 2001 From: arkadiuszos4chain Date: Fri, 5 Apr 2024 08:44:34 +0200 Subject: [PATCH] feat(BUX-400): add tests --- contacts_test.go | 67 +++++++++++++++++++++++++++++- fixtures/fixtures.go | 1 + totp.go | 23 +++++++--- totp/totp.go | 15 +++---- totp/totp_test.go | 38 +++++++++-------- totp_test.go | 99 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 209 insertions(+), 34 deletions(-) create mode 100644 totp_test.go diff --git a/contacts_test.go b/contacts_test.go index ed91fa3..f57172d 100644 --- a/contacts_test.go +++ b/contacts_test.go @@ -5,10 +5,12 @@ import ( "testing" "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" + "github.com/bitcoin-sv/spv-wallet/models" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// TestRejectContact will test the RejectContact method +// TestContactActionsRouting will test routing func TestContactActionsRouting(t *testing.T) { tcs := []struct { name string @@ -55,6 +57,16 @@ func TestContactActionsRouting(t *testing.T) { return err }, }, + {name: "ConfirmContact", + route: "/contact/confirmed/", + responsePayload: "{}", + f: func(c *WalletClient) error { + contact := models.Contact{PubKey: fixtures.PubKey} + + passcode, _ := c.GenerateTotpForContact(&contact, 30, 2) + return c.ConfirmContact(context.Background(), &contact, passcode, 30, 2) + }, + }, } for _, tc := range tcs { @@ -68,7 +80,7 @@ func TestContactActionsRouting(t *testing.T) { Client: WithHTTPClient, } - client := getTestWalletClient(tmq, true) + client := getTestWalletClient(tmq, false) // when err := tc.f(client) @@ -79,3 +91,54 @@ func TestContactActionsRouting(t *testing.T) { } } + +func TestConfirmContact(t *testing.T) { + t.Run("TOTP is valid - call Confirm Action", func(t *testing.T) { + // given + tmq := testTransportHandler{ + Type: fixtures.RequestType, + Path: "/contact/confirmed/", + Result: "{}", + ClientURL: fixtures.ServerURL, + Client: WithHTTPClient, + } + + client := getTestWalletClient(tmq, false) + + contact := &models.Contact{PubKey: fixtures.PubKey} + totp, err := client.GenerateTotpForContact(contact, 30, 2) + require.NoError(t, err) + + // when + result := client.ConfirmContact(context.Background(), contact, totp, 30, 2) + + // then + require.Nil(t, result) + }) + + t.Run("TOTP is invalid - do not call Confirm Action", func(t *testing.T) { + // given + tmq := testTransportHandler{ + Type: fixtures.RequestType, + Path: "/unknown/", + Result: "{}", + ClientURL: fixtures.ServerURL, + Client: WithHTTPClient, + } + + client := getTestWalletClient(tmq, false) + + alice := &models.Contact{PubKey: "034252e5359a1de3b8ec08e6c29b80594e88fb47e6ae9ce65ee5a94f0d371d2cde"} + a_totp, err := client.GenerateTotpForContact(alice, 30, 2) + require.NoError(t, err) + + bob := &models.Contact{PubKey: "02dde493752f7bc89822ed8a13e0e4aa04550c6c4430800e4be1e5e5c2556cf65b"} + + // when + result := client.ConfirmContact(context.Background(), bob, a_totp, 30, 2) + + // then + require.NotNil(t, result) + require.Equal(t, result.Error(), "totp is invalid") + }) +} diff --git a/fixtures/fixtures.go b/fixtures/fixtures.go index c9e494c..356517b 100644 --- a/fixtures/fixtures.go +++ b/fixtures/fixtures.go @@ -14,6 +14,7 @@ const ( XPrivString = "xprv9s21ZrQH143K3N6qVJQAu4EP51qMcyrKYJLkLgmYXgz58xmVxVLSsbx2DfJUtjcnXK8NdvkHMKfmmg5AJT2nqqRWUrjSHX29qEJwBgBPkJQ" AccessKeyString = "7779d24ca6f8821f225042bf55e8f80aa41b08b879b72827f51e41e6523b9cd0" PaymailAddress = "address@paymail.com" + PubKey = "034252e5359a1de3b8ec08e6c29b80594e88fb47e6ae9ce65ee5a94f0d371d2cde" ) func MarshallForTestHandler(object any) string { diff --git a/totp.go b/totp.go index a4ba40a..f82469a 100644 --- a/totp.go +++ b/totp.go @@ -1,6 +1,7 @@ package walletclient import ( + "encoding/hex" "errors" "fmt" @@ -8,13 +9,16 @@ import ( "github.com/bitcoin-sv/spv-wallet-go-client/utils" "github.com/bitcoin-sv/spv-wallet/models" "github.com/bitcoinschema/go-bitcoin/v2" + "github.com/libsv/go-bk/bec" "github.com/libsv/go-bk/bip32" ) +var ErrClientInitNoXpriv = errors.New("init client with xPriv first") + // GenerateTotpForContact creates one time-based one-time password based on secret shared between the user and the contact func (b *WalletClient) GenerateTotpForContact(contact *models.Contact, validationPeriod, digits uint) (string, error) { if b.xPriv == nil { - return "", errors.New("init client with xPriv first") + return "", ErrClientInitNoXpriv } xpriv, err := deriveXprivForPki(b.xPriv) @@ -22,7 +26,7 @@ func (b *WalletClient) GenerateTotpForContact(contact *models.Contact, validatio return "", err } - cXpub, err := bip32.NewKeyFromString(contact.PubKey) + cXpub, err := convertPubKey(contact.PubKey) if err != nil { return "", fmt.Errorf("contact's PubKey is invalid: %w", err) } @@ -32,13 +36,13 @@ func (b *WalletClient) GenerateTotpForContact(contact *models.Contact, validatio Digits: digits, } - return ts.GenarateTotp(xpriv, cXpub) + return ts.GenerateTotp(xpriv, cXpub) } // ValidateTotpForContact validates one time-based one-time password based on secret shared between the user and the contact func (b *WalletClient) ValidateTotpForContact(contact *models.Contact, passcode string, validationPeriod, digits uint) (bool, error) { if b.xPriv == nil { - return false, errors.New("init client with xPriv first") + return false, ErrClientInitNoXpriv } xpriv, err := deriveXprivForPki(b.xPriv) @@ -46,7 +50,7 @@ func (b *WalletClient) ValidateTotpForContact(contact *models.Contact, passcode return false, err } - cXpub, err := bip32.NewKeyFromString(contact.PubKey) + cXpub, err := convertPubKey(contact.PubKey) if err != nil { return false, fmt.Errorf("contact's PubKey is invalid: %w", err) } @@ -70,3 +74,12 @@ func deriveXprivForPki(xpriv *bip32.ExtendedKey) (*bip32.ExtendedKey, error) { return pkiXpriv.Child(0) } + +func convertPubKey(pubKey string) (*bec.PublicKey, error) { + hex, err := hex.DecodeString(pubKey) + if err != nil { + return nil, err + } + + return bec.ParsePubKey(hex, bec.S256()) +} diff --git a/totp/totp.go b/totp/totp.go index f2cee51..3d65258 100644 --- a/totp/totp.go +++ b/totp/totp.go @@ -17,8 +17,8 @@ type Service struct { } // GenerateTotp creates one time-based one-time password based on secrets calculated from the keys -func (s *Service) GenarateTotp(xPriv, xPub *bip32.ExtendedKey) (string, error) { - secret, err := sharedSecret(xPriv, xPub) +func (s *Service) GenerateTotp(xPriv *bip32.ExtendedKey, pubKey *bec.PublicKey) (string, error) { + secret, err := sharedSecret(xPriv, pubKey) if err != nil { return "", err } @@ -27,8 +27,8 @@ func (s *Service) GenarateTotp(xPriv, xPub *bip32.ExtendedKey) (string, error) { } // ValidateTotp checks if given one-time password is valid -func (s *Service) ValidateTotp(xPriv, xPub *bip32.ExtendedKey, passcode string) (bool, error) { - secret, err := sharedSecret(xPriv, xPub) +func (s *Service) ValidateTotp(xPriv *bip32.ExtendedKey, pubKey *bec.PublicKey, passcode string) (bool, error) { + secret, err := sharedSecret(xPriv, pubKey) if err != nil { return false, err } @@ -43,17 +43,12 @@ func (s *Service) getOpts() totp.ValidateOpts { } } -func sharedSecret(xPriv, xPub *bip32.ExtendedKey) (string, error) { +func sharedSecret(xPriv *bip32.ExtendedKey, pubKey *bec.PublicKey) (string, error) { privKey, err := xPriv.ECPrivKey() if err != nil { return "", err } - pubKey, err := xPub.ECPubKey() - if err != nil { - return "", err - } - x, _ := bec.S256().ScalarMult(pubKey.X, pubKey.Y, privKey.D.Bytes()) return base32.StdEncoding.EncodeToString(x.Bytes()), nil diff --git a/totp/totp_test.go b/totp/totp_test.go index 1f6827d..738c80c 100644 --- a/totp/totp_test.go +++ b/totp/totp_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "github.com/libsv/go-bk/bec" "github.com/libsv/go-bk/bip32" "github.com/stretchr/testify/require" ) @@ -16,16 +17,15 @@ func TestTotpService(t *testing.T) { // given const givenDigits = 4 - a_xpriv, _ := bip32.NewKeyFromString(a_xprivStr) - b_xpriv, _ := bip32.NewKeyFromString(b_xprivStr) - b_xpub, _ := b_xpriv.Neuter() + a_xpriv, _ := getKeyPair(a_xprivStr) + _, b_pk := getKeyPair(b_xprivStr) sut := &Service{ Digits: givenDigits, } // when - pc, err := sut.GenarateTotp(a_xpriv, b_xpub) + pc, err := sut.GenerateTotp(a_xpriv, b_pk) // then require.NoError(t, err) @@ -34,19 +34,17 @@ func TestTotpService(t *testing.T) { t.Run("Passcode is valid", func(t *testing.T) { // given - a_xpriv, _ := bip32.NewKeyFromString(a_xprivStr) - a_xpub, _ := a_xpriv.Neuter() - b_xpriv, _ := bip32.NewKeyFromString(b_xprivStr) - b_xpub, _ := b_xpriv.Neuter() + a_xpriv, a_pk := getKeyPair(a_xprivStr) + b_xpriv, b_pk := getKeyPair(b_xprivStr) sut := &Service{ Digits: 2, } - a_passcode, err := sut.GenarateTotp(a_xpriv, b_xpub) + a_passcode, err := sut.GenerateTotp(a_xpriv, b_pk) require.NoError(t, err) // when - isValid, err := sut.ValidateTotp(b_xpriv, a_xpub, a_passcode) + isValid, err := sut.ValidateTotp(b_xpriv, a_pk, a_passcode) require.NoError(t, err) // then @@ -58,26 +56,24 @@ func TestTotpService(t *testing.T) { // given const givenSeconds = 3 - a_xpriv, _ := bip32.NewKeyFromString(a_xprivStr) - a_xpub, _ := a_xpriv.Neuter() - b_xpriv, _ := bip32.NewKeyFromString(b_xprivStr) - b_xpub, _ := b_xpriv.Neuter() + a_xpriv, a_pk := getKeyPair(a_xprivStr) + b_xpriv, b_pk := getKeyPair(b_xprivStr) sut := &Service{ Digits: 2, Period: givenSeconds, } - a_passcode, err := sut.GenarateTotp(a_xpriv, b_xpub) + a_passcode, err := sut.GenerateTotp(a_xpriv, b_pk) require.NoError(t, err) // when - isValid, err := sut.ValidateTotp(b_xpriv, a_xpub, a_passcode) + isValid, err := sut.ValidateTotp(b_xpriv, a_pk, a_passcode) require.NoError(t, err) require.True(t, isValid) time.Sleep(givenSeconds * time.Second) - isValid, err = sut.ValidateTotp(b_xpriv, a_xpub, a_passcode) + isValid, err = sut.ValidateTotp(b_xpriv, a_pk, a_passcode) // then require.NoError(t, err) @@ -85,3 +81,11 @@ func TestTotpService(t *testing.T) { }) } + +func getKeyPair(xprivStr string) (xpriv *bip32.ExtendedKey, pk *bec.PublicKey) { + xpriv, _ = bip32.NewKeyFromString(xprivStr) + xpub, _ := xpriv.Neuter() + pk, _ = xpub.ECPubKey() + + return +} diff --git a/totp_test.go b/totp_test.go new file mode 100644 index 0000000..f528d5f --- /dev/null +++ b/totp_test.go @@ -0,0 +1,99 @@ +package walletclient + +import ( + "testing" + + "github.com/bitcoin-sv/spv-wallet-go-client/fixtures" + "github.com/bitcoin-sv/spv-wallet/models" + "github.com/stretchr/testify/require" +) + +func TestGenerateTotpForContact(t *testing.T) { + t.Run("success", func(t *testing.T) { + // given + sut, err := New(WithXPriv(fixtures.XPrivString), WithHTTP("localhost:3001")) + require.NoError(t, err) + + contact := models.Contact{PubKey: fixtures.PubKey} + + // when + pass, err := sut.GenerateTotpForContact(&contact, 30, 2) + + // then + require.NoError(t, err) + require.Len(t, pass, 2) + }) + + t.Run("WalletClient without xPriv - returns error", func(t *testing.T) { + // given + sut, err := New(WithXPub(fixtures.XPubString), WithHTTP("localhost:3001")) + require.NoError(t, err) + + // when + _, err = sut.GenerateTotpForContact(nil, 30, 2) + + // then + require.ErrorIs(t, err, ErrClientInitNoXpriv) + }) + + t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { + // given + sut, err := New(WithXPriv(fixtures.XPrivString), WithHTTP("localhost:3001")) + require.NoError(t, err) + + contact := models.Contact{PubKey: "invalid-pk-format"} + + // when + _, err = sut.GenerateTotpForContact(&contact, 30, 2) + + // then + require.ErrorContains(t, err, "contact's PubKey is invalid:") + + }) +} + +func TestValidateTotpForContact(t *testing.T) { + t.Run("success", func(t *testing.T) { + // given + sut, err := New(WithXPriv(fixtures.XPrivString), WithHTTP("localhost:3001")) + require.NoError(t, err) + + contact := models.Contact{PubKey: fixtures.PubKey} + pass, err := sut.GenerateTotpForContact(&contact, 30, 2) + require.NoError(t, err) + + // when + result, err := sut.ValidateTotpForContact(&contact, pass, 30, 2) + + // then + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("WalletClient without xPriv - returns error", func(t *testing.T) { + // given + sut, err := New(WithXPub(fixtures.XPubString), WithHTTP("localhost:3001")) + require.NoError(t, err) + + // when + _, err = sut.ValidateTotpForContact(nil, "", 30, 2) + + // then + require.ErrorIs(t, err, ErrClientInitNoXpriv) + }) + + t.Run("contact has invalid PubKey - returns error", func(t *testing.T) { + // given + sut, err := New(WithXPriv(fixtures.XPrivString), WithHTTP("localhost:3001")) + require.NoError(t, err) + + contact := models.Contact{PubKey: "invalid-pk-format"} + + // when + _, err = sut.ValidateTotpForContact(&contact, "", 30, 2) + + // then + require.ErrorContains(t, err, "contact's PubKey is invalid:") + + }) +}