github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/rest_api_test.go (about)

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