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 }