Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(SPV-1079): add endpoint tests for get merkleroots #745

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions actions/merkleroots/get_merkleroots_endpoint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package merkleroots_test

import (
"encoding/json"
"testing"

"github.com/bitcoin-sv/spv-wallet/actions/testabilities"
"github.com/bitcoin-sv/spv-wallet/engine/chain/errors"
"github.com/bitcoin-sv/spv-wallet/models"
"github.com/go-resty/resty/v2"
"github.com/stretchr/testify/require"
)

const merklerootsURL = "/api/v1/merkleroots"

func setupWalletAndClientForUser(t *testing.T, responseCode *int, response *string) (*testabilities.SPVWalletApplicationAssertions, func(), *resty.Client) {
given, then := testabilities.New(t)
cleanup := given.StartedSPVWallet()
client := given.HttpClient().ForUser()

if response != nil && responseCode != nil {
given.BHS().WillRespondForMerkleRoots(*responseCode, *response)
}

return &then, cleanup, client
}

func TestGETMerkleRootsSuccess(t *testing.T) {

testCases := map[string]struct {
query string
expectedResponse models.MerkleRootsBHSResponse
}{
"Get MerkleRoots success no query params": {
query: "",
expectedResponse: models.MerkleRootsBHSResponse{
Content: testabilities.MockedBHSData,
Page: models.ExclusiveStartKeyPageInfo{
TotalElements: len(testabilities.MockedBHSData),
Size: len(testabilities.MockedBHSData),
LastEvaluatedKey: "",
},
},
},
"Get MerkleRoots success with last evaluated key param": {
query: "?lastEvaluatedKey=df2b060fa2e5e9c8ed5eaf6a45c13753ec8c63282b2688322eba40cd98ea067a",
expectedResponse: models.MerkleRootsBHSResponse{
Content: testabilities.MockedBHSData[5:],
Page: models.ExclusiveStartKeyPageInfo{
TotalElements: len(testabilities.MockedBHSData),
Size: len(testabilities.MockedBHSData[5:]),
LastEvaluatedKey: "",
},
},
},
}

for name, tt := range testCases {
t.Run(name, func(t *testing.T) {

// given
expResponseJSON, err := json.Marshal(tt.expectedResponse)
require.NoError(t, err, "Failed to marshall expected response")
then, cleanup, client := setupWalletAndClientForUser(t, nil, nil)
defer cleanup()
url := merklerootsURL

if tt.query != "" {
url = url + tt.query
}

// when
res, err := client.R().
SetHeader("Content-Type", "application/json").
Get(url)
require.NoError(t, err, "Unexpected error occurred while getting MerkleRoots")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is not needed for those tests, please read the documentation of given.HttpClient method, if it's not enaugh clear, then read below.

The given.HttpClient method is adding error handler to resty so that any unexpected error that could be returned here will fail the tests so you don't need to make any assumptions about returned error and can focus only on res.


// then
(*then).Response(res).IsOK().WithJSONf(string(expResponseJSON))
})
}

}

func TestGETMerkleRootsFailure(t *testing.T) {

testCases := map[string]struct {
bhsToken string
bhsURL string
expectErr models.SPVError
response string
responseCode int
batchSize string
lastEvaluatedKey string
}{
"Get MerkleRoots with wrong batch size": {
bhsURL: merklerootsURL,
expectErr: chainerrors.ErrInvalidBatchSize,
responseCode: 400,
response: "{\"code\": \"ErrInvalidBatchSize\",\"message\": \"batchSize must be 0 or a positive integer\"}",
},
"Get MerkleRoots with invalid merkleroot": {
bhsURL: merklerootsURL,
expectErr: chainerrors.ErrMerkleRootNotFound,
responseCode: 404,
response: "{\"code\": \"ErrMerkleRootNotFound\",\"message\": \"No block with provided merkleroot was found\"}",
},
"Get MerkleRoots with stale merkleroot": {
bhsURL: merklerootsURL,
expectErr: chainerrors.ErrMerkleRootNotInLongestChain,
responseCode: 409,
response: "{\"code\": \"ErrMerkleRootNotInLC\",\"message\": \"Provided merkleroot is not part of the longest chain\"}",
},
}

for name, tt := range testCases {
t.Run(name, func(t *testing.T) {
// given
then, cleanup, client := setupWalletAndClientForUser(t, &tt.responseCode, &tt.response)
defer cleanup()

// when
resErr := &models.ResponseError{}
res, err := client.R().
SetHeader("Content-Type", "application/json").
SetError(resErr).
Get(tt.bhsURL)
require.NoError(t, err, "Unexpected error occurred while getting MerkleRoots")

// then
(*then).Response(res).IsNotSuccess()
require.NotNil(t, resErr)
require.Equal(t, tt.expectErr.GetCode(), resErr.Code)
require.Equal(t, tt.expectErr.GetMessage(), resErr.Message)
})
}

t.Run("not allowed for anonymous", func(t *testing.T) {
// given:
given, then := testabilities.New(t)
cleanup := given.StartedSPVWallet()
defer cleanup()

// and:
client := given.HttpClient().ForAnonymous()

// when:
res, err := client.R().Get(merklerootsURL)
require.NoError(t, err, "Unexpected error occurred while getting MerkleRoots")

// then:
then.Response(res).IsUnauthorized()
})

t.Run("not allowed for admin", func(t *testing.T) {
// given:
given, then := testabilities.New(t)
cleanup := given.StartedSPVWallet()
defer cleanup()

// and:
client := given.HttpClient().ForAdmin()

// when:
res, err := client.R().Get(merklerootsURL)
require.NoError(t, err, "Unexpected error occurred while getting MerkleRoots")

// then:
then.Response(res).IsUnauthorizedForAdmin()
})
}
2 changes: 1 addition & 1 deletion actions/merkleroots/merkleroots_test.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please please please
Remove those tests now, as the new one are much better then that.

Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (ts *TestSuite) SetupTest() {
ts.BaseSetupTest()

handlersManager := handlers.NewManager(ts.Router, config.APIVersion)
RegisterRoutes(ts.AppConfig, handlersManager)
RegisterRoutes(handlersManager)
}

// TearDownTest runs after each test
Expand Down
3 changes: 1 addition & 2 deletions actions/merkleroots/routes.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package merkleroots

import (
"github.com/bitcoin-sv/spv-wallet/config"
"github.com/bitcoin-sv/spv-wallet/server/handlers"
)

// RegisterRoutes creates the specific package routes
func RegisterRoutes(appConfig *config.AppConfig, handlersManager *handlers.Manager) {
func RegisterRoutes(handlersManager *handlers.Manager) {
group := handlersManager.Group(handlers.GroupAPI, "/merkleroots")
group.GET("", handlers.AsUser(get))
}
2 changes: 1 addition & 1 deletion actions/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func Register(appConfig *config.AppConfig, handlersManager *handlers.Manager) {
utxos.RegisterRoutes(handlersManager)
users.RegisterRoutes(handlersManager)
sharedconfig.RegisterRoutes(handlersManager)
merkleroots.RegisterRoutes(appConfig, handlersManager)
merkleroots.RegisterRoutes(handlersManager)
if appConfig.ExperimentalFeatures.PikeContactsEnabled {
contacts.RegisterRoutes(handlersManager)
}
Expand Down
6 changes: 6 additions & 0 deletions actions/testabilities/assert_spvwallet_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type SPVWalletApplicationAssertions interface {

type SPVWalletResponseAssertions interface {
IsOK() SPVWalletResponseAssertions
IsNotSuccess() SPVWalletResponseAssertions
WithJSONf(expectedFormat string, args ...any)
// IsUnauthorized asserts that the response status code is 401 and the error is about lack of authorization.
IsUnauthorized()
Expand Down Expand Up @@ -63,6 +64,11 @@ func (a *responseAssertions) IsOK() SPVWalletResponseAssertions {
return a.assertIsStatus(http.StatusOK)
}

func (a *responseAssertions) IsNotSuccess() SPVWalletResponseAssertions {
a.assert.False(a.response.IsSuccess())
return a
}

func (a *responseAssertions) IsBadRequest() SPVWalletResponseAssertions {
return a.assertIsStatus(http.StatusBadRequest)
}
Expand Down
123 changes: 123 additions & 0 deletions actions/testabilities/fixture_block_header_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package testabilities

import (
"slices"

"github.com/bitcoin-sv/spv-wallet/models"
)

// MockedBHSData is mocked merkle roots data on Block Header Service (BHS) side
var MockedBHSData = []models.MerkleRoot{
{
BlockHeight: 0,
MerkleRoot: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
},
{
BlockHeight: 1,
MerkleRoot: "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098",
},
{
BlockHeight: 2,
MerkleRoot: "9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5",
},
{
BlockHeight: 3,
MerkleRoot: "999e1c837c76a1b7fbb7e57baf87b309960f5ffefbf2a9b95dd890602272f644",
},
{
BlockHeight: 4,
MerkleRoot: "df2b060fa2e5e9c8ed5eaf6a45c13753ec8c63282b2688322eba40cd98ea067a",
},
{
BlockHeight: 5,
MerkleRoot: "63522845d294ee9b0188ae5cac91bf389a0c3723f084ca1025e7d9cdfe481ce1",
},
{
BlockHeight: 6,
MerkleRoot: "20251a76e64e920e58291a30d4b212939aae976baca40e70818ceaa596fb9d37",
},
{
BlockHeight: 7,
MerkleRoot: "8aa673bc752f2851fd645d6a0a92917e967083007d9c1684f9423b100540673f",
},
{
BlockHeight: 8,
MerkleRoot: "a6f7f1c0dad0f2eb6b13c4f33de664b1b0e9f22efad5994a6d5b6086d85e85e3",
},
{
BlockHeight: 9,
MerkleRoot: "0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9",
},
{
BlockHeight: 10,
MerkleRoot: "d3ad39fa52a89997ac7381c95eeffeaf40b66af7a57e9eba144be0a175a12b11",
},
{
BlockHeight: 11,
MerkleRoot: "f8325d8f7fa5d658ea143629288d0530d2710dc9193ddc067439de803c37066e",
},
{
BlockHeight: 12,
MerkleRoot: "3b96bb7e197ef276b85131afd4a09c059cc368133a26ca04ebffb0ab4f75c8b8",
},
{
BlockHeight: 13,
MerkleRoot: "9962d5c704ec27243364cbe9d384808feeac1c15c35ac790dffd1e929829b271",
},
{
BlockHeight: 14,
MerkleRoot: "e1afd89295b68bc5247fe0ca2885dd4b8818d7ce430faa615067d7bab8640156",
},
}

// LastMockedMerkleRoot returns last merkleroot value from MockedBHSData
func LastMockedMerkleRoot() models.MerkleRoot {
return MockedBHSData[len(MockedBHSData)-1]
}

// mockedMerkleRootsAPIResponseFn is a mock of Block Header Service (BHS) get merkleroots endpoint
// it will return a paged response of merkle roots since last evaluated merkle root
func mockedMerkleRootsAPIResponseFn(lastMerkleRoot string) models.ExclusiveStartKeyPage[[]models.MerkleRoot] {
if lastMerkleRoot == "" {
return models.ExclusiveStartKeyPage[[]models.MerkleRoot]{
Content: MockedBHSData,
Page: models.ExclusiveStartKeyPageInfo{
LastEvaluatedKey: "",
TotalElements: len(MockedBHSData),
Size: len(MockedBHSData),
},
}
}

lastMerkleRootIdx := slices.IndexFunc(MockedBHSData, func(mr models.MerkleRoot) bool {
return mr.MerkleRoot == lastMerkleRoot
})

// handle case when lastMerkleRoot is already highest in the servers database
if lastMerkleRootIdx == len(MockedBHSData)-1 {
return models.ExclusiveStartKeyPage[[]models.MerkleRoot]{
Content: []models.MerkleRoot{},
Page: models.ExclusiveStartKeyPageInfo{
LastEvaluatedKey: "",
TotalElements: len(MockedBHSData),
Size: 0,
},
}
}

content := MockedBHSData[lastMerkleRootIdx+1:]
lastEvaluatedKey := content[len(content)-1].MerkleRoot

if lastEvaluatedKey == MockedBHSData[len(MockedBHSData)-1].MerkleRoot {
lastEvaluatedKey = ""
}

return models.ExclusiveStartKeyPage[[]models.MerkleRoot]{
Content: content,
Page: models.ExclusiveStartKeyPageInfo{
LastEvaluatedKey: lastEvaluatedKey,
TotalElements: len(MockedBHSData),
Size: len(content),
},
}
}
Loading
Loading