github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/rest/routes/blocks_test.go (about) 1 package routes 2 3 import ( 4 "fmt" 5 "net/http" 6 "net/url" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/onflow/flow-go/engine/access/rest/request" 12 "github.com/onflow/flow-go/engine/access/rest/util" 13 14 mocks "github.com/stretchr/testify/mock" 15 "github.com/stretchr/testify/require" 16 "google.golang.org/grpc/codes" 17 "google.golang.org/grpc/status" 18 19 "github.com/onflow/flow-go/access/mock" 20 "github.com/onflow/flow-go/engine/access/rest/middleware" 21 "github.com/onflow/flow-go/model/flow" 22 "github.com/onflow/flow-go/utils/unittest" 23 ) 24 25 type testVector struct { 26 description string 27 request *http.Request 28 expectedStatus int 29 expectedResponse string 30 } 31 32 func prepareTestVectors(t *testing.T, 33 blockIDs []string, 34 heights []string, 35 blocks []*flow.Block, 36 executionResults []*flow.ExecutionResult, 37 blkCnt int) []testVector { 38 39 singleBlockExpandedResponse := expectedBlockResponsesExpanded(blocks[:1], executionResults[:1], true, flow.BlockStatusUnknown) 40 singleSealedBlockExpandedResponse := expectedBlockResponsesExpanded(blocks[:1], executionResults[:1], true, flow.BlockStatusSealed) 41 multipleBlockExpandedResponse := expectedBlockResponsesExpanded(blocks, executionResults, true, flow.BlockStatusUnknown) 42 multipleSealedBlockExpandedResponse := expectedBlockResponsesExpanded(blocks, executionResults, true, flow.BlockStatusSealed) 43 44 singleBlockCondensedResponse := expectedBlockResponsesExpanded(blocks[:1], executionResults[:1], false, flow.BlockStatusUnknown) 45 multipleBlockCondensedResponse := expectedBlockResponsesExpanded(blocks, executionResults, false, flow.BlockStatusUnknown) 46 47 invalidID := unittest.IdentifierFixture().String() 48 invalidHeight := fmt.Sprintf("%d", blkCnt+1) 49 50 maxIDs := flow.IdentifierList(unittest.IdentifierListFixture(request.MaxBlockRequestHeightRange + 1)) 51 52 testVectors := []testVector{ 53 { 54 description: "Get single expanded block by ID", 55 request: getByIDsExpandedURL(t, blockIDs[:1]), 56 expectedStatus: http.StatusOK, 57 expectedResponse: singleBlockExpandedResponse, 58 }, 59 { 60 description: "Get multiple expanded blocks by IDs", 61 request: getByIDsExpandedURL(t, blockIDs), 62 expectedStatus: http.StatusOK, 63 expectedResponse: multipleBlockExpandedResponse, 64 }, 65 { 66 description: "Get single condensed block by ID", 67 request: getByIDsCondensedURL(t, blockIDs[:1]), 68 expectedStatus: http.StatusOK, 69 expectedResponse: singleBlockCondensedResponse, 70 }, 71 { 72 description: "Get multiple condensed blocks by IDs", 73 request: getByIDsCondensedURL(t, blockIDs), 74 expectedStatus: http.StatusOK, 75 expectedResponse: multipleBlockCondensedResponse, 76 }, 77 { 78 description: "Get single expanded block by height", 79 request: getByHeightsExpandedURL(t, heights[:1]...), 80 expectedStatus: http.StatusOK, 81 expectedResponse: singleSealedBlockExpandedResponse, 82 }, 83 { 84 description: "Get multiple expanded blocks by heights", 85 request: getByHeightsExpandedURL(t, heights...), 86 expectedStatus: http.StatusOK, 87 expectedResponse: multipleSealedBlockExpandedResponse, 88 }, 89 { 90 description: "Get multiple expanded blocks by start and end height", 91 request: getByStartEndHeightExpandedURL(t, heights[0], heights[len(heights)-1]), 92 expectedStatus: http.StatusOK, 93 expectedResponse: multipleSealedBlockExpandedResponse, 94 }, 95 { 96 description: "Get block by ID not found", 97 request: getByIDsExpandedURL(t, []string{invalidID}), 98 expectedStatus: http.StatusNotFound, 99 expectedResponse: fmt.Sprintf(`{"code":404, "message":"error looking up block with ID %s"}`, invalidID), 100 }, 101 { 102 description: "Get block by height not found", 103 request: getByHeightsExpandedURL(t, invalidHeight), 104 expectedStatus: http.StatusNotFound, 105 expectedResponse: fmt.Sprintf(`{"code":404, "message":"error looking up block at height %s"}`, invalidHeight), 106 }, 107 { 108 description: "Get block by end height less than start height", 109 request: getByStartEndHeightExpandedURL(t, heights[len(heights)-1], heights[0]), 110 expectedStatus: http.StatusBadRequest, 111 expectedResponse: `{"code":400, "message": "start height must be less than or equal to end height"}`, 112 }, 113 { 114 description: "Get block by both heights and start and end height", 115 request: requestURL(t, nil, heights[len(heights)-1], heights[0], true, heights...), 116 expectedStatus: http.StatusBadRequest, 117 expectedResponse: `{"code":400, "message": "can only provide either heights or start and end height range"}`, 118 }, 119 { 120 description: "Get block with missing height param", 121 request: getByHeightsExpandedURL(t), // no height query param specified 122 expectedStatus: http.StatusBadRequest, 123 expectedResponse: `{"code":400, "message": "must provide either heights or start and end height range"}`, 124 }, 125 { 126 description: "Get block with missing height values", 127 request: getByHeightsExpandedURL(t, ""), // height query param specified with no value 128 expectedStatus: http.StatusBadRequest, 129 expectedResponse: `{"code":400, "message": "must provide either heights or start and end height range"}`, 130 }, 131 { 132 description: "Get block by more than maximum permissible number of IDs", 133 request: getByIDsCondensedURL(t, maxIDs.Strings()), // height query param specified with no value 134 expectedStatus: http.StatusBadRequest, 135 expectedResponse: fmt.Sprintf(`{"code":400, "message": "at most %d IDs can be requested at a time"}`, request.MaxBlockRequestHeightRange), 136 }, 137 } 138 return testVectors 139 } 140 141 // TestGetBlocks tests local get blocks by ID and get blocks by heights API 142 func TestAccessGetBlocks(t *testing.T) { 143 backend := &mock.API{} 144 145 blkCnt := 10 146 blockIDs, heights, blocks, executionResults := generateMocks(backend, blkCnt) 147 testVectors := prepareTestVectors(t, blockIDs, heights, blocks, executionResults, blkCnt) 148 149 for _, tv := range testVectors { 150 rr := executeRequest(tv.request, backend) 151 require.Equal(t, tv.expectedStatus, rr.Code, "failed test %s: incorrect response code", tv.description) 152 actualResp := rr.Body.String() 153 require.JSONEq(t, tv.expectedResponse, actualResp, "Failed: %s: incorrect response body", tv.description) 154 } 155 } 156 157 func requestURL(t *testing.T, ids []string, start string, end string, expandResponse bool, heights ...string) *http.Request { 158 u, _ := url.Parse("/v1/blocks") 159 q := u.Query() 160 161 if len(ids) > 0 { 162 u, _ = url.Parse(u.String() + "/" + strings.Join(ids, ",")) 163 } 164 165 if start != "" { 166 q.Add(startHeightQueryParam, start) 167 q.Add(endHeightQueryParam, end) 168 } 169 170 if len(heights) > 0 { 171 heightsStr := strings.Join(heights, ",") 172 q.Add(heightQueryParam, heightsStr) 173 } 174 175 if expandResponse { 176 var expands []string 177 expands = append(expands, ExpandableFieldPayload) 178 expands = append(expands, ExpandableExecutionResult) 179 expandsStr := strings.Join(expands, ",") 180 q.Add(middleware.ExpandQueryParam, expandsStr) 181 } 182 183 u.RawQuery = q.Encode() 184 185 req, err := http.NewRequest("GET", u.String(), nil) 186 require.NoError(t, err) 187 return req 188 } 189 190 func getByIDsExpandedURL(t *testing.T, ids []string) *http.Request { 191 return requestURL(t, ids, "", "", true) 192 } 193 194 func getByHeightsExpandedURL(t *testing.T, heights ...string) *http.Request { 195 return requestURL(t, nil, "", "", true, heights...) 196 } 197 198 func getByStartEndHeightExpandedURL(t *testing.T, start, end string) *http.Request { 199 return requestURL(t, nil, start, end, true) 200 } 201 202 func getByIDsCondensedURL(t *testing.T, ids []string) *http.Request { 203 return requestURL(t, ids, "", "", false) 204 } 205 206 func generateMocks(backend *mock.API, count int) ([]string, []string, []*flow.Block, []*flow.ExecutionResult) { 207 blockIDs := make([]string, count) 208 heights := make([]string, count) 209 blocks := make([]*flow.Block, count) 210 executionResults := make([]*flow.ExecutionResult, count) 211 212 for i := 0; i < count; i++ { 213 block := unittest.BlockFixture() 214 block.Header.Height = uint64(i) 215 blocks[i] = &block 216 blockIDs[i] = block.Header.ID().String() 217 heights[i] = fmt.Sprintf("%d", block.Header.Height) 218 219 executionResult := unittest.ExecutionResultFixture() 220 executionResult.BlockID = block.ID() 221 executionResults[i] = executionResult 222 223 backend.Mock.On("GetBlockByID", mocks.Anything, block.ID()).Return(&block, flow.BlockStatusSealed, nil) 224 backend.Mock.On("GetBlockByHeight", mocks.Anything, block.Header.Height).Return(&block, flow.BlockStatusSealed, nil) 225 backend.Mock.On("GetExecutionResultForBlockID", mocks.Anything, block.ID()).Return(executionResults[i], nil) 226 } 227 228 // any other call to the backend should return a not found error 229 backend.Mock.On("GetBlockByID", mocks.Anything, mocks.Anything).Return(nil, flow.BlockStatusUnknown, status.Error(codes.NotFound, "not found")) 230 backend.Mock.On("GetBlockByHeight", mocks.Anything, mocks.Anything).Return(nil, flow.BlockStatusUnknown, status.Error(codes.NotFound, "not found")) 231 232 return blockIDs, heights, blocks, executionResults 233 } 234 235 func expectedBlockResponsesExpanded(blocks []*flow.Block, execResult []*flow.ExecutionResult, expanded bool, status flow.BlockStatus) string { 236 blockResponses := make([]string, len(blocks)) 237 for i, b := range blocks { 238 blockResponses[i] = expectedBlockResponse(b, execResult[i], expanded, status) 239 } 240 return fmt.Sprintf("[%s]", strings.Join(blockResponses, ",")) 241 } 242 243 func expectedBlockResponse(block *flow.Block, execResult *flow.ExecutionResult, expanded bool, status flow.BlockStatus) string { 244 id := block.ID().String() 245 execResultID := execResult.ID().String() 246 execLink := fmt.Sprintf("/v1/execution_results/%s", execResultID) 247 blockLink := fmt.Sprintf("/v1/blocks/%s", id) 248 payloadLink := fmt.Sprintf("/v1/blocks/%s/payload", id) 249 blockStatus := status.String() 250 251 timestamp := block.Header.Timestamp.Format(time.RFC3339Nano) 252 253 if expanded { 254 return fmt.Sprintf(` 255 { 256 "header": { 257 "id": "%s", 258 "parent_id": "%s", 259 "height": "%d", 260 "timestamp": "%s", 261 "parent_voter_signature": "%s" 262 }, 263 "payload": { 264 "collection_guarantees": [], 265 "block_seals": [] 266 }, 267 "execution_result": %s, 268 "_expandable": {}, 269 "_links": { 270 "_self": "%s" 271 }, 272 "block_status": "%s" 273 }`, id, block.Header.ParentID.String(), block.Header.Height, timestamp, 274 util.ToBase64(block.Header.ParentVoterSigData), executionResultExpectedStr(execResult), blockLink, blockStatus) 275 } 276 277 return fmt.Sprintf(` 278 { 279 "header": { 280 "id": "%s", 281 "parent_id": "%s", 282 "height": "%d", 283 "timestamp": "%s", 284 "parent_voter_signature": "%s" 285 }, 286 "_expandable": { 287 "payload": "%s", 288 "execution_result": "%s" 289 }, 290 "_links": { 291 "_self": "%s" 292 }, 293 "block_status": "%s" 294 }`, id, block.Header.ParentID.String(), block.Header.Height, timestamp, 295 util.ToBase64(block.Header.ParentVoterSigData), payloadLink, execLink, blockLink, blockStatus) 296 }