github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/rest/routes/events_test.go (about) 1 package routes 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "net/url" 8 "strings" 9 "testing" 10 "time" 11 12 mocks "github.com/stretchr/testify/mock" 13 "github.com/stretchr/testify/require" 14 15 "google.golang.org/grpc/codes" 16 "google.golang.org/grpc/status" 17 18 "github.com/onflow/flow/protobuf/go/flow/entities" 19 20 "github.com/onflow/flow-go/access/mock" 21 "github.com/onflow/flow-go/engine/access/rest/util" 22 "github.com/onflow/flow-go/model/flow" 23 "github.com/onflow/flow-go/utils/unittest" 24 ) 25 26 func TestGetEvents(t *testing.T) { 27 backend := &mock.API{} 28 events := generateEventsMocks(backend, 5) 29 30 allBlockIDs := make([]string, len(events)) 31 for i, e := range events { 32 allBlockIDs[i] = e.BlockID.String() 33 } 34 startHeight := fmt.Sprint(events[0].BlockHeight) 35 endHeight := fmt.Sprint(events[len(events)-1].BlockHeight) 36 37 // remove events from the last block to test that an empty BlockEvents is returned when the last 38 // block contains no events 39 truncatedEvents := append(events[:len(events)-1], flow.BlockEvents{ 40 BlockHeight: events[len(events)-1].BlockHeight, 41 BlockID: events[len(events)-1].BlockID, 42 BlockTimestamp: events[len(events)-1].BlockTimestamp, 43 }) 44 45 testVectors := []testVector{ 46 // valid 47 { 48 description: "Get events for a single block by ID", 49 request: getEventReq(t, "A.179b6b1cb6755e31.Foo.Bar", "", "", []string{events[0].BlockID.String()}), 50 expectedStatus: http.StatusOK, 51 expectedResponse: testBlockEventResponse(t, []flow.BlockEvents{events[0]}), 52 }, 53 { 54 description: "Get events by all block IDs", 55 request: getEventReq(t, "A.179b6b1cb6755e31.Foo.Bar", "", "", allBlockIDs), 56 expectedStatus: http.StatusOK, 57 expectedResponse: testBlockEventResponse(t, events), 58 }, 59 { 60 description: "Get events for height range", 61 request: getEventReq(t, "A.179b6b1cb6755e31.Foo.Bar", startHeight, endHeight, nil), 62 expectedStatus: http.StatusOK, 63 expectedResponse: testBlockEventResponse(t, events), 64 }, 65 { 66 description: "Get events range ending at sealed block", 67 request: getEventReq(t, "A.179b6b1cb6755e31.Foo.Bar", "0", "sealed", nil), 68 expectedStatus: http.StatusOK, 69 expectedResponse: testBlockEventResponse(t, events), 70 }, 71 { 72 description: "Get events range ending after last block", 73 request: getEventReq(t, "A.179b6b1cb6755e31.Foo.Bar", "0", fmt.Sprint(events[len(events)-1].BlockHeight+5), nil), 74 expectedStatus: http.StatusOK, 75 expectedResponse: testBlockEventResponse(t, truncatedEvents), 76 }, 77 // invalid 78 { 79 description: "Get invalid - missing all fields", 80 request: getEventReq(t, "", "", "", nil), 81 expectedStatus: http.StatusBadRequest, 82 expectedResponse: `{"code":400,"message":"must provide either block IDs or start and end height range"}`, 83 }, 84 { 85 description: "Get invalid - missing query event type", 86 request: getEventReq(t, "", "", "", []string{events[0].BlockID.String()}), 87 expectedStatus: http.StatusBadRequest, 88 expectedResponse: `{"code":400,"message":"event type must be provided"}`, 89 }, 90 { 91 description: "Get invalid - missing end height", 92 request: getEventReq(t, "A.179b6b1cb6755e31.Foo.Bar", "100", "", nil), 93 expectedStatus: http.StatusBadRequest, 94 expectedResponse: `{"code":400,"message":"must provide either block IDs or start and end height range"}`, 95 }, 96 { 97 description: "Get invalid - start height bigger than end height", 98 request: getEventReq(t, "A.179b6b1cb6755e31.Foo.Bar", "100", "50", nil), 99 expectedStatus: http.StatusBadRequest, 100 expectedResponse: `{"code":400,"message":"start height must be less than or equal to end height"}`, 101 }, 102 { 103 description: "Get invalid - too big interval", 104 request: getEventReq(t, "A.179b6b1cb6755e31.Foo.Bar", "0", "5000", nil), 105 expectedStatus: http.StatusBadRequest, 106 expectedResponse: `{"code":400,"message":"height range 5000 exceeds maximum allowed of 250"}`, 107 }, 108 { 109 description: "Get invalid - can not provide all params", 110 request: getEventReq(t, "A.179b6b1cb6755e31.Foo.Bar", "100", "120", []string{"10e782612a014b5c9c7d17994d7e67157064f3dd42fa92cd080bfb0fe22c3f71"}), 111 expectedStatus: http.StatusBadRequest, 112 expectedResponse: `{"code":400,"message":"can only provide either block IDs or start and end height range"}`, 113 }, 114 { 115 description: "Get invalid - invalid height format", 116 request: getEventReq(t, "A.179b6b1cb6755e31.Foo.Bar", "foo", "120", nil), 117 expectedStatus: http.StatusBadRequest, 118 expectedResponse: `{"code":400,"message":"invalid start height: invalid height format"}`, 119 }, 120 { 121 description: "Get invalid - latest block smaller than start", 122 request: getEventReq(t, "A.179b6b1cb6755e31.Foo.Bar", "100000", "sealed", nil), 123 expectedStatus: http.StatusBadRequest, 124 expectedResponse: `{"code":400,"message":"current retrieved end height value is lower than start height"}`, 125 }, 126 } 127 128 for _, test := range testVectors { 129 t.Run(test.description, func(t *testing.T) { 130 assertResponse(t, test.request, test.expectedStatus, test.expectedResponse, backend) 131 }) 132 } 133 134 } 135 136 func getEventReq(t *testing.T, eventType string, start string, end string, blockIDs []string) *http.Request { 137 u, _ := url.Parse("/v1/events") 138 q := u.Query() 139 140 if len(blockIDs) > 0 { 141 q.Add(BlockQueryParam, strings.Join(blockIDs, ",")) 142 } 143 144 if start != "" && end != "" { 145 q.Add(startHeightQueryParam, start) 146 q.Add(endHeightQueryParam, end) 147 } 148 149 q.Add(EventTypeQuery, eventType) 150 151 u.RawQuery = q.Encode() 152 153 req, err := http.NewRequest("GET", u.String(), nil) 154 require.NoError(t, err) 155 156 return req 157 } 158 159 func generateEventsMocks(backend *mock.API, n int) []flow.BlockEvents { 160 events := make([]flow.BlockEvents, n) 161 ids := make([]flow.Identifier, n) 162 163 var lastHeader *flow.Header 164 for i := 0; i < n; i++ { 165 header := unittest.BlockHeaderFixture(unittest.WithHeaderHeight(uint64(i))) 166 ids[i] = header.ID() 167 168 events[i] = unittest.BlockEventsFixture(header, 2) 169 170 backend.Mock. 171 On("GetEventsForBlockIDs", mocks.Anything, mocks.Anything, []flow.Identifier{header.ID()}, entities.EventEncodingVersion_JSON_CDC_V0). 172 Return([]flow.BlockEvents{events[i]}, nil) 173 174 lastHeader = header 175 } 176 177 backend.Mock. 178 On("GetEventsForBlockIDs", mocks.Anything, mocks.Anything, ids, entities.EventEncodingVersion_JSON_CDC_V0). 179 Return(events, nil) 180 181 // range from first to last block 182 backend.Mock.On( 183 "GetEventsForHeightRange", 184 mocks.Anything, 185 mocks.Anything, 186 events[0].BlockHeight, 187 events[len(events)-1].BlockHeight, 188 entities.EventEncodingVersion_JSON_CDC_V0, 189 ).Return(events, nil) 190 191 // range from first to last block + 5 192 backend.Mock.On( 193 "GetEventsForHeightRange", 194 mocks.Anything, 195 mocks.Anything, 196 events[0].BlockHeight, 197 events[len(events)-1].BlockHeight+5, 198 entities.EventEncodingVersion_JSON_CDC_V0, 199 ).Return(append(events[:len(events)-1], unittest.BlockEventsFixture(lastHeader, 0)), nil) 200 201 latestBlock := unittest.BlockHeaderFixture() 202 latestBlock.Height = uint64(n - 1) 203 204 // default not found 205 backend.Mock. 206 On("GetEventsForBlockIDs", mocks.Anything, mocks.Anything, mocks.Anything, entities.EventEncodingVersion_JSON_CDC_V0). 207 Return(nil, status.Error(codes.NotFound, "not found")) 208 209 backend.Mock. 210 On("GetEventsForHeightRange", mocks.Anything, mocks.Anything). 211 Return(nil, status.Error(codes.NotFound, "not found")) 212 213 backend.Mock. 214 On("GetLatestBlockHeader", mocks.Anything, true). 215 Return(latestBlock, flow.BlockStatusSealed, nil) 216 217 return events 218 } 219 220 func testBlockEventResponse(t *testing.T, events []flow.BlockEvents) string { 221 222 type eventResponse struct { 223 Type flow.EventType `json:"type"` 224 TransactionID flow.Identifier `json:"transaction_id"` 225 TransactionIndex string `json:"transaction_index"` 226 EventIndex string `json:"event_index"` 227 Payload string `json:"payload"` 228 } 229 230 type blockEventsResponse struct { 231 BlockID flow.Identifier `json:"block_id"` 232 BlockHeight string `json:"block_height"` 233 BlockTimestamp string `json:"block_timestamp"` 234 Events []eventResponse `json:"events,omitempty"` 235 } 236 237 res := make([]blockEventsResponse, len(events)) 238 239 for i, e := range events { 240 events := make([]eventResponse, len(e.Events)) 241 242 for i, ev := range e.Events { 243 events[i] = eventResponse{ 244 Type: ev.Type, 245 TransactionID: ev.TransactionID, 246 TransactionIndex: fmt.Sprint(ev.TransactionIndex), 247 EventIndex: fmt.Sprint(ev.EventIndex), 248 Payload: util.ToBase64(ev.Payload), 249 } 250 } 251 252 res[i] = blockEventsResponse{ 253 BlockID: e.BlockID, 254 BlockHeight: fmt.Sprint(e.BlockHeight), 255 BlockTimestamp: e.BlockTimestamp.Format(time.RFC3339Nano), 256 Events: events, 257 } 258 } 259 260 data, err := json.Marshal(res) 261 require.NoError(t, err) 262 263 return string(data) 264 }