github.com/koko1123/flow-go-1@v0.29.6/engine/access/rest/blocks_test.go (about) 1 package rest 2 3 import ( 4 "fmt" 5 "net/http" 6 "net/url" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/stretchr/testify/assert" 12 13 "github.com/koko1123/flow-go-1/engine/access/rest/request" 14 "github.com/koko1123/flow-go-1/engine/access/rest/util" 15 16 mocks "github.com/stretchr/testify/mock" 17 "github.com/stretchr/testify/require" 18 "google.golang.org/grpc/codes" 19 "google.golang.org/grpc/status" 20 21 "github.com/koko1123/flow-go-1/access/mock" 22 "github.com/koko1123/flow-go-1/engine/access/rest/middleware" 23 "github.com/koko1123/flow-go-1/model/flow" 24 "github.com/koko1123/flow-go-1/utils/unittest" 25 ) 26 27 type testVector struct { 28 description string 29 request *http.Request 30 expectedStatus int 31 expectedResponse string 32 } 33 34 // TestGetBlocks tests the get blocks by ID and get blocks by heights API 35 func TestGetBlocks(t *testing.T) { 36 backend := &mock.API{} 37 38 blkCnt := 10 39 blockIDs, heights, blocks, executionResults := generateMocks(backend, blkCnt) 40 41 singleBlockExpandedResponse := expectedBlockResponsesExpanded(blocks[:1], executionResults[:1], true) 42 multipleBlockExpandedResponse := expectedBlockResponsesExpanded(blocks, executionResults, true) 43 44 singleBlockCondensedResponse := expectedBlockResponsesExpanded(blocks[:1], executionResults[:1], false) 45 multipleBlockCondensedResponse := expectedBlockResponsesExpanded(blocks, executionResults, false) 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: singleBlockExpandedResponse, 82 }, 83 { 84 description: "Get multiple expanded blocks by heights", 85 request: getByHeightsExpandedURL(t, heights...), 86 expectedStatus: http.StatusOK, 87 expectedResponse: multipleBlockExpandedResponse, 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: multipleBlockExpandedResponse, 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 139 for _, tv := range testVectors { 140 responseRec, err := executeRequest(tv.request, backend) 141 assert.NoError(t, err) 142 require.Equal(t, tv.expectedStatus, responseRec.Code, "failed test %s: incorrect response code", tv.description) 143 actualResp := responseRec.Body.String() 144 require.JSONEq(t, tv.expectedResponse, actualResp, "Failed: %s: incorrect response body", tv.description) 145 } 146 } 147 148 func requestURL(t *testing.T, ids []string, start string, end string, expandResponse bool, heights ...string) *http.Request { 149 u, _ := url.Parse("/v1/blocks") 150 q := u.Query() 151 152 if len(ids) > 0 { 153 u, _ = url.Parse(u.String() + "/" + strings.Join(ids, ",")) 154 } 155 156 if start != "" { 157 q.Add(startHeightQueryParam, start) 158 q.Add(endHeightQueryParam, end) 159 } 160 161 if len(heights) > 0 { 162 heightsStr := strings.Join(heights, ",") 163 q.Add(heightQueryParam, heightsStr) 164 } 165 166 if expandResponse { 167 var expands []string 168 expands = append(expands, ExpandableFieldPayload) 169 expands = append(expands, ExpandableExecutionResult) 170 expandsStr := strings.Join(expands, ",") 171 q.Add(middleware.ExpandQueryParam, expandsStr) 172 } 173 174 u.RawQuery = q.Encode() 175 176 req, err := http.NewRequest("GET", u.String(), nil) 177 require.NoError(t, err) 178 return req 179 } 180 181 func getByIDsExpandedURL(t *testing.T, ids []string) *http.Request { 182 return requestURL(t, ids, "", "", true) 183 } 184 185 func getByHeightsExpandedURL(t *testing.T, heights ...string) *http.Request { 186 return requestURL(t, nil, "", "", true, heights...) 187 } 188 189 func getByStartEndHeightExpandedURL(t *testing.T, start, end string) *http.Request { 190 return requestURL(t, nil, start, end, true) 191 } 192 193 func getByIDsCondensedURL(t *testing.T, ids []string) *http.Request { 194 return requestURL(t, ids, "", "", false) 195 } 196 197 func generateMocks(backend *mock.API, count int) ([]string, []string, []*flow.Block, []*flow.ExecutionResult) { 198 blockIDs := make([]string, count) 199 heights := make([]string, count) 200 blocks := make([]*flow.Block, count) 201 executionResults := make([]*flow.ExecutionResult, count) 202 203 for i := 0; i < count; i++ { 204 block := unittest.BlockFixture() 205 block.Header.Height = uint64(i) 206 blocks[i] = &block 207 blockIDs[i] = block.Header.ID().String() 208 heights[i] = fmt.Sprintf("%d", block.Header.Height) 209 210 executionResult := unittest.ExecutionResultFixture() 211 executionResult.BlockID = block.ID() 212 executionResults[i] = executionResult 213 214 backend.Mock.On("GetBlockByID", mocks.Anything, block.ID()).Return(&block, flow.BlockStatusSealed, nil) 215 backend.Mock.On("GetBlockByHeight", mocks.Anything, block.Header.Height).Return(&block, flow.BlockStatusSealed, nil) 216 backend.Mock.On("GetExecutionResultForBlockID", mocks.Anything, block.ID()).Return(executionResults[i], nil) 217 } 218 219 // any other call to the backend should return a not found error 220 backend.Mock.On("GetBlockByID", mocks.Anything, mocks.Anything).Return(nil, flow.BlockStatusUnknown, status.Error(codes.NotFound, "not found")) 221 backend.Mock.On("GetBlockByHeight", mocks.Anything, mocks.Anything).Return(nil, flow.BlockStatusUnknown, status.Error(codes.NotFound, "not found")) 222 223 return blockIDs, heights, blocks, executionResults 224 } 225 226 func expectedBlockResponsesExpanded(blocks []*flow.Block, execResult []*flow.ExecutionResult, expanded bool) string { 227 blockResponses := make([]string, len(blocks)) 228 for i, b := range blocks { 229 blockResponses[i] = expectedBlockResponse(b, execResult[i], expanded) 230 } 231 return fmt.Sprintf("[%s]", strings.Join(blockResponses, ",")) 232 } 233 234 func expectedBlockResponse(block *flow.Block, execResult *flow.ExecutionResult, expanded bool) string { 235 id := block.ID().String() 236 execResultID := execResult.ID().String() 237 execLink := fmt.Sprintf("/v1/execution_results/%s", execResultID) 238 blockLink := fmt.Sprintf("/v1/blocks/%s", id) 239 payloadLink := fmt.Sprintf("/v1/blocks/%s/payload", id) 240 241 timestamp := block.Header.Timestamp.Format(time.RFC3339Nano) 242 243 if expanded { 244 return fmt.Sprintf(` 245 { 246 "header": { 247 "id": "%s", 248 "parent_id": "%s", 249 "height": "%d", 250 "timestamp": "%s", 251 "parent_voter_signature": "%s" 252 }, 253 "payload": { 254 "collection_guarantees": [], 255 "block_seals": [] 256 }, 257 "execution_result": %s, 258 "_expandable": {}, 259 "_links": { 260 "_self": "%s" 261 } 262 }`, id, block.Header.ParentID.String(), block.Header.Height, timestamp, 263 util.ToBase64(block.Header.ParentVoterSigData), executionResultExpectedStr(execResult), blockLink) 264 } 265 266 return fmt.Sprintf(` 267 { 268 "header": { 269 "id": "%s", 270 "parent_id": "%s", 271 "height": "%d", 272 "timestamp": "%s", 273 "parent_voter_signature": "%s" 274 }, 275 "_expandable": { 276 "payload": "%s", 277 "execution_result": "%s" 278 }, 279 "_links": { 280 "_self": "%s" 281 } 282 }`, id, block.Header.ParentID.String(), block.Header.Height, timestamp, 283 util.ToBase64(block.Header.ParentVoterSigData), payloadLink, execLink, blockLink) 284 }