github.com/koko1123/flow-go-1@v0.29.6/engine/access/rest_api_test.go (about)

     1  package access
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	"math/rand"
     8  	"net/http"
     9  	"os"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/antihax/optional"
    15  	restclient "github.com/onflow/flow/openapi/go-client-generated"
    16  	"github.com/rs/zerolog"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/mock"
    19  	"github.com/stretchr/testify/require"
    20  	"github.com/stretchr/testify/suite"
    21  
    22  	accessmock "github.com/koko1123/flow-go-1/engine/access/mock"
    23  	"github.com/koko1123/flow-go-1/engine/access/rest"
    24  	"github.com/koko1123/flow-go-1/engine/access/rest/request"
    25  	"github.com/koko1123/flow-go-1/engine/access/rpc"
    26  	"github.com/koko1123/flow-go-1/model/flow"
    27  	"github.com/koko1123/flow-go-1/module/metrics"
    28  	module "github.com/koko1123/flow-go-1/module/mock"
    29  	"github.com/koko1123/flow-go-1/network"
    30  	protocol "github.com/koko1123/flow-go-1/state/protocol/mock"
    31  	"github.com/koko1123/flow-go-1/storage"
    32  	storagemock "github.com/koko1123/flow-go-1/storage/mock"
    33  	"github.com/koko1123/flow-go-1/utils/unittest"
    34  )
    35  
    36  // RestAPITestSuite tests that the Access node serves the REST API defined via the OpenApi spec accurately
    37  // The tests starts the REST server locally and uses the Go REST client on onflow/flow repo to make the api requests.
    38  // The server uses storage mocks
    39  type RestAPITestSuite struct {
    40  	suite.Suite
    41  	state             *protocol.State
    42  	sealedSnaphost    *protocol.Snapshot
    43  	finalizedSnapshot *protocol.Snapshot
    44  	log               zerolog.Logger
    45  	net               *network.Network
    46  	request           *module.Requester
    47  	collClient        *accessmock.AccessAPIClient
    48  	execClient        *accessmock.ExecutionAPIClient
    49  	me                *module.Local
    50  	chainID           flow.ChainID
    51  	metrics           *metrics.NoopCollector
    52  	rpcEng            *rpc.Engine
    53  
    54  	// storage
    55  	blocks           *storagemock.Blocks
    56  	headers          *storagemock.Headers
    57  	collections      *storagemock.Collections
    58  	transactions     *storagemock.Transactions
    59  	receipts         *storagemock.ExecutionReceipts
    60  	executionResults *storagemock.ExecutionResults
    61  }
    62  
    63  func (suite *RestAPITestSuite) SetupTest() {
    64  	suite.log = zerolog.New(os.Stdout)
    65  	suite.net = new(network.Network)
    66  	suite.state = new(protocol.State)
    67  	suite.sealedSnaphost = new(protocol.Snapshot)
    68  	suite.finalizedSnapshot = new(protocol.Snapshot)
    69  
    70  	suite.state.On("Sealed").Return(suite.sealedSnaphost, nil)
    71  	suite.state.On("Final").Return(suite.finalizedSnapshot, nil)
    72  	suite.blocks = new(storagemock.Blocks)
    73  	suite.headers = new(storagemock.Headers)
    74  	suite.transactions = new(storagemock.Transactions)
    75  	suite.collections = new(storagemock.Collections)
    76  	suite.receipts = new(storagemock.ExecutionReceipts)
    77  	suite.executionResults = new(storagemock.ExecutionResults)
    78  
    79  	suite.collClient = new(accessmock.AccessAPIClient)
    80  	suite.execClient = new(accessmock.ExecutionAPIClient)
    81  
    82  	suite.request = new(module.Requester)
    83  	suite.request.On("EntityByID", mock.Anything, mock.Anything)
    84  
    85  	suite.me = new(module.Local)
    86  
    87  	accessIdentity := unittest.IdentityFixture(unittest.WithRole(flow.RoleAccess))
    88  	suite.me.
    89  		On("NodeID").
    90  		Return(accessIdentity.NodeID)
    91  
    92  	suite.chainID = flow.Testnet
    93  	suite.metrics = metrics.NewNoopCollector()
    94  
    95  	config := rpc.Config{
    96  		UnsecureGRPCListenAddr: unittest.DefaultAddress,
    97  		SecureGRPCListenAddr:   unittest.DefaultAddress,
    98  		HTTPListenAddr:         unittest.DefaultAddress,
    99  		RESTListenAddr:         unittest.DefaultAddress,
   100  	}
   101  
   102  	rpcEngBuilder, err := rpc.NewBuilder(suite.log, suite.state, config, suite.collClient, nil, suite.blocks, suite.headers, suite.collections, suite.transactions,
   103  		nil, suite.executionResults, suite.chainID, suite.metrics, suite.metrics, 0, 0, false,
   104  		false, nil, nil)
   105  	assert.NoError(suite.T(), err)
   106  	suite.rpcEng, err = rpcEngBuilder.WithLegacy().Build()
   107  	assert.NoError(suite.T(), err)
   108  	unittest.AssertClosesBefore(suite.T(), suite.rpcEng.Ready(), 2*time.Second)
   109  
   110  	// wait for the server to startup
   111  	assert.Eventually(suite.T(), func() bool {
   112  		return suite.rpcEng.RestApiAddress() != nil
   113  	}, 5*time.Second, 10*time.Millisecond)
   114  }
   115  
   116  func TestRestAPI(t *testing.T) {
   117  	suite.Run(t, new(RestAPITestSuite))
   118  }
   119  
   120  func (suite *RestAPITestSuite) TestGetBlock() {
   121  
   122  	testBlockIDs := make([]string, request.MaxIDsLength)
   123  	testBlocks := make([]*flow.Block, request.MaxIDsLength)
   124  	for i := range testBlockIDs {
   125  		collections := unittest.CollectionListFixture(1)
   126  		block := unittest.BlockWithGuaranteesFixture(
   127  			unittest.CollectionGuaranteesWithCollectionIDFixture(collections),
   128  		)
   129  		block.Header.Height = uint64(i)
   130  		suite.blocks.On("ByID", block.ID()).Return(block, nil)
   131  		suite.blocks.On("ByHeight", block.Header.Height).Return(block, nil)
   132  		testBlocks[i] = block
   133  		testBlockIDs[i] = block.ID().String()
   134  
   135  		execResult := unittest.ExecutionResultFixture()
   136  		suite.executionResults.On("ByBlockID", block.ID()).Return(execResult, nil)
   137  	}
   138  
   139  	sealedBlock := testBlocks[len(testBlocks)-1]
   140  	finalizedBlock := testBlocks[len(testBlocks)-2]
   141  	suite.sealedSnaphost.On("Head").Return(sealedBlock.Header, nil)
   142  	suite.finalizedSnapshot.On("Head").Return(finalizedBlock.Header, nil)
   143  
   144  	client := suite.restAPIClient()
   145  
   146  	suite.Run("GetBlockByID for a single ID - happy path", func() {
   147  
   148  		testBlock := testBlocks[0]
   149  		suite.blocks.On("ByID", testBlock.ID()).Return(testBlock, nil).Once()
   150  
   151  		ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   152  		defer cancel()
   153  
   154  		respBlocks, resp, err := client.BlocksApi.BlocksIdGet(ctx, []string{testBlock.ID().String()}, optionsForBlockByID())
   155  		require.NoError(suite.T(), err)
   156  		require.Equal(suite.T(), http.StatusOK, resp.StatusCode)
   157  		require.Len(suite.T(), respBlocks, 1)
   158  		assert.Equal(suite.T(), testBlock.ID().String(), testBlock.ID().String())
   159  
   160  		require.Nil(suite.T(), respBlocks[0].ExecutionResult)
   161  	})
   162  
   163  	suite.Run("GetBlockByID for multiple IDs - happy path", func() {
   164  
   165  		ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   166  		defer cancel()
   167  
   168  		// the swagger generated Go client code has bug where it generates a space delimited list of ids instead of a
   169  		// comma delimited one. hence, explicitly setting the ids as a csv here
   170  		blockIDSlice := []string{strings.Join(testBlockIDs, ",")}
   171  
   172  		actualBlocks, resp, err := client.BlocksApi.BlocksIdGet(ctx, blockIDSlice, optionsForBlockByID())
   173  		require.NoError(suite.T(), err)
   174  		assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
   175  		assert.Len(suite.T(), actualBlocks, request.MaxIDsLength)
   176  		for i, b := range testBlocks {
   177  			assert.Equal(suite.T(), b.ID().String(), actualBlocks[i].Header.Id)
   178  		}
   179  	})
   180  
   181  	suite.Run("GetBlockByHeight by start and end height - happy path", func() {
   182  
   183  		ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   184  		defer cancel()
   185  
   186  		startHeight := testBlocks[0].Header.Height
   187  		blkCnt := len(testBlocks)
   188  		endHeight := testBlocks[blkCnt-1].Header.Height
   189  
   190  		actualBlocks, resp, err := client.BlocksApi.BlocksGet(ctx, optionsForBlockByStartEndHeight(startHeight, endHeight))
   191  		require.NoError(suite.T(), err)
   192  		assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
   193  		assert.Len(suite.T(), actualBlocks, blkCnt)
   194  		for i := 0; i < blkCnt; i++ {
   195  			assert.Equal(suite.T(), testBlocks[i].ID().String(), actualBlocks[i].Header.Id)
   196  			assert.Equal(suite.T(), fmt.Sprintf("%d", testBlocks[i].Header.Height), actualBlocks[i].Header.Height)
   197  		}
   198  	})
   199  
   200  	suite.Run("GetBlockByHeight by heights - happy path", func() {
   201  
   202  		ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   203  		defer cancel()
   204  
   205  		lastIndex := len(testBlocks)
   206  		var reqHeights = make([]uint64, len(testBlocks))
   207  		for i := 0; i < lastIndex; i++ {
   208  			reqHeights[i] = testBlocks[i].Header.Height
   209  		}
   210  
   211  		actualBlocks, resp, err := client.BlocksApi.BlocksGet(ctx, optionsForBlockByHeights(reqHeights))
   212  		require.NoError(suite.T(), err)
   213  		assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
   214  		assert.Len(suite.T(), actualBlocks, lastIndex)
   215  		for i := 0; i < lastIndex; i++ {
   216  			assert.Equal(suite.T(), testBlocks[i].ID().String(), actualBlocks[i].Header.Id)
   217  			assert.Equal(suite.T(), fmt.Sprintf("%d", testBlocks[i].Header.Height), actualBlocks[i].Header.Height)
   218  		}
   219  	})
   220  
   221  	suite.Run("GetBlockByHeight for height=final - happy path", func() {
   222  
   223  		ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   224  		defer cancel()
   225  
   226  		actualBlocks, resp, err := client.BlocksApi.BlocksGet(ctx, optionsForFinalizedBlock("final"))
   227  		require.NoError(suite.T(), err)
   228  		assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
   229  		assert.Len(suite.T(), actualBlocks, 1)
   230  		assert.Equal(suite.T(), finalizedBlock.ID().String(), actualBlocks[0].Header.Id)
   231  	})
   232  
   233  	suite.Run("GetBlockByHeight for height=sealed happy path", func() {
   234  
   235  		ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   236  		defer cancel()
   237  
   238  		actualBlocks, resp, err := client.BlocksApi.BlocksGet(ctx, optionsForFinalizedBlock("sealed"))
   239  		require.NoError(suite.T(), err)
   240  		assert.Equal(suite.T(), http.StatusOK, resp.StatusCode)
   241  		assert.Len(suite.T(), actualBlocks, 1)
   242  		assert.Equal(suite.T(), sealedBlock.ID().String(), actualBlocks[0].Header.Id)
   243  	})
   244  
   245  	suite.Run("GetBlockByID with a non-existing block ID", func() {
   246  
   247  		nonExistingBlockID := unittest.IdentifierFixture()
   248  		suite.blocks.On("ByID", nonExistingBlockID).Return(nil, storage.ErrNotFound).Once()
   249  
   250  		client := suite.restAPIClient()
   251  		ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   252  		defer cancel()
   253  
   254  		_, resp, err := client.BlocksApi.BlocksIdGet(ctx, []string{nonExistingBlockID.String()}, optionsForBlockByID())
   255  		assertError(suite.T(), resp, err, http.StatusNotFound, fmt.Sprintf("error looking up block with ID %s", nonExistingBlockID.String()))
   256  	})
   257  
   258  	suite.Run("GetBlockByID with an invalid block ID", func() {
   259  
   260  		ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   261  		defer cancel()
   262  
   263  		const invalidBlockID = "invalid_block_id"
   264  		_, resp, err := client.BlocksApi.BlocksIdGet(ctx, []string{invalidBlockID}, optionsForBlockByID())
   265  		assertError(suite.T(), resp, err, http.StatusBadRequest, "invalid ID format")
   266  	})
   267  
   268  	suite.Run("GetBlockByID with more than the permissible number of block IDs", func() {
   269  
   270  		ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   271  		defer cancel()
   272  
   273  		blockIDs := make([]string, request.MaxIDsLength+1)
   274  		copy(blockIDs, testBlockIDs)
   275  		blockIDs[request.MaxIDsLength] = unittest.IdentifierFixture().String()
   276  
   277  		blockIDSlice := []string{strings.Join(blockIDs, ",")}
   278  		_, resp, err := client.BlocksApi.BlocksIdGet(ctx, blockIDSlice, optionsForBlockByID())
   279  		assertError(suite.T(), resp, err, http.StatusBadRequest, fmt.Sprintf("at most %d IDs can be requested at a time", request.MaxIDsLength))
   280  	})
   281  
   282  	suite.Run("GetBlockByID with one non-existing block ID", func() {
   283  
   284  		ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   285  		defer cancel()
   286  
   287  		// replace one ID with a block ID for which the storage returns a not found error
   288  		rand.Seed(time.Now().Unix())
   289  		invalidBlockIndex := rand.Intn(len(testBlocks))
   290  		invalidID := unittest.IdentifierFixture()
   291  		suite.blocks.On("ByID", invalidID).Return(nil, storage.ErrNotFound).Once()
   292  		blockIDs := make([]string, len(testBlockIDs))
   293  		copy(blockIDs, testBlockIDs)
   294  		blockIDs[invalidBlockIndex] = invalidID.String()
   295  
   296  		blockIDSlice := []string{strings.Join(blockIDs, ",")}
   297  		_, resp, err := client.BlocksApi.BlocksIdGet(ctx, blockIDSlice, optionsForBlockByID())
   298  		assertError(suite.T(), resp, err, http.StatusNotFound, fmt.Sprintf("error looking up block with ID %s", blockIDs[invalidBlockIndex]))
   299  	})
   300  
   301  	suite.Run("GetBlockByHeight by non-existing height", func() {
   302  
   303  		ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   304  		defer cancel()
   305  
   306  		invalidHeight := uint64(len(testBlocks))
   307  		var reqHeights = []uint64{invalidHeight}
   308  		suite.blocks.On("ByHeight", invalidHeight).Return(nil, storage.ErrNotFound).Once()
   309  
   310  		_, resp, err := client.BlocksApi.BlocksGet(ctx, optionsForBlockByHeights(reqHeights))
   311  		assertError(suite.T(), resp, err, http.StatusNotFound, fmt.Sprintf("error looking up block at height %d", invalidHeight))
   312  	})
   313  }
   314  
   315  func (suite *RestAPITestSuite) TestRequestSizeRestriction() {
   316  	client := suite.restAPIClient()
   317  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   318  	defer cancel()
   319  	// make a request of size larger than the max permitted size
   320  	requestBytes := make([]byte, rest.MaxRequestSize+1)
   321  	script := restclient.ScriptsBody{
   322  		Script: string(requestBytes),
   323  	}
   324  	_, resp, err := client.ScriptsApi.ScriptsPost(ctx, script, nil)
   325  	assertError(suite.T(), resp, err, http.StatusBadRequest, "request body too large")
   326  }
   327  
   328  func (suite *RestAPITestSuite) TearDownTest() {
   329  	// close the server
   330  	if suite.rpcEng != nil {
   331  		unittest.AssertClosesBefore(suite.T(), suite.rpcEng.Done(), 2*time.Second)
   332  	}
   333  }
   334  
   335  // restAPIClient creates a REST API client
   336  func (suite *RestAPITestSuite) restAPIClient() *restclient.APIClient {
   337  	config := restclient.NewConfiguration()
   338  	config.BasePath = fmt.Sprintf("http://%s/v1", suite.rpcEng.RestApiAddress().String())
   339  	return restclient.NewAPIClient(config)
   340  }
   341  
   342  func assertError(t *testing.T, resp *http.Response, err error, expectedCode int, expectedMsgSubstr string) {
   343  	require.NotNil(t, resp)
   344  	assert.Equal(t, expectedCode, resp.StatusCode)
   345  	require.Error(t, err)
   346  	swaggerError := err.(restclient.GenericSwaggerError)
   347  	modelError := swaggerError.Model().(restclient.ModelError)
   348  	require.EqualValues(t, expectedCode, modelError.Code)
   349  	require.Contains(t, modelError.Message, expectedMsgSubstr)
   350  }
   351  
   352  func optionsForBlockByID() *restclient.BlocksApiBlocksIdGetOpts {
   353  	return &restclient.BlocksApiBlocksIdGetOpts{
   354  		Expand:  optional.NewInterface([]string{rest.ExpandableFieldPayload}),
   355  		Select_: optional.NewInterface([]string{"header.id"}),
   356  	}
   357  }
   358  func optionsForBlockByStartEndHeight(startHeight, endHeight uint64) *restclient.BlocksApiBlocksGetOpts {
   359  	return &restclient.BlocksApiBlocksGetOpts{
   360  		Expand:      optional.NewInterface([]string{rest.ExpandableFieldPayload}),
   361  		Select_:     optional.NewInterface([]string{"header.id", "header.height"}),
   362  		StartHeight: optional.NewInterface(startHeight),
   363  		EndHeight:   optional.NewInterface(endHeight),
   364  	}
   365  }
   366  
   367  func optionsForBlockByHeights(heights []uint64) *restclient.BlocksApiBlocksGetOpts {
   368  	return &restclient.BlocksApiBlocksGetOpts{
   369  		Expand:  optional.NewInterface([]string{rest.ExpandableFieldPayload}),
   370  		Select_: optional.NewInterface([]string{"header.id", "header.height"}),
   371  		Height:  optional.NewInterface(heights),
   372  	}
   373  }
   374  
   375  func optionsForFinalizedBlock(finalOrSealed string) *restclient.BlocksApiBlocksGetOpts {
   376  	return &restclient.BlocksApiBlocksGetOpts{
   377  		Expand:  optional.NewInterface([]string{rest.ExpandableFieldPayload}),
   378  		Select_: optional.NewInterface([]string{"header.id", "header.height"}),
   379  		Height:  optional.NewInterface(finalOrSealed),
   380  	}
   381  }