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 }