github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/state_stream/backend/backend_account_statuses_test.go (about) 1 package backend 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 "time" 8 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/mock" 11 "github.com/stretchr/testify/require" 12 "github.com/stretchr/testify/suite" 13 "google.golang.org/grpc/codes" 14 "google.golang.org/grpc/status" 15 16 "github.com/onflow/flow-go/engine/access/state_stream" 17 "github.com/onflow/flow-go/engine/access/subscription" 18 "github.com/onflow/flow-go/model/flow" 19 "github.com/onflow/flow-go/module/executiondatasync/execution_data" 20 "github.com/onflow/flow-go/utils/unittest" 21 "github.com/onflow/flow-go/utils/unittest/generator" 22 ) 23 24 var testProtocolEventTypes = []flow.EventType{ 25 state_stream.CoreEventAccountCreated, 26 state_stream.CoreEventAccountContractAdded, 27 state_stream.CoreEventAccountContractUpdated, 28 } 29 30 // Define the test type struct 31 // The struct is used for testing different test cases of each endpoint from AccountStatusesBackend. 32 type testType struct { 33 name string // Test case name 34 highestBackfill int // Highest backfill index 35 startValue interface{} 36 filters state_stream.AccountStatusFilter // Event filters 37 } 38 39 // BackendAccountStatusesSuite is a test suite for the AccountStatusesBackend functionality. 40 // It is used to test the endpoints which enables users to subscribe to the streaming of account status changes. 41 // It verified that each of endpoints works properly with expected data being returned. Also the suite tests 42 // handling of expected errors in the SubscribeAccountStatuses. 43 type BackendAccountStatusesSuite struct { 44 BackendExecutionDataSuite 45 accountCreatedAddress flow.Address 46 accountContractAdded flow.Address 47 accountContractUpdated flow.Address 48 } 49 50 func TestBackendAccountStatusesSuite(t *testing.T) { 51 suite.Run(t, new(BackendAccountStatusesSuite)) 52 } 53 54 // generateProtocolMockEvents generates a set of mock events. 55 func (s *BackendAccountStatusesSuite) generateProtocolMockEvents() flow.EventsList { 56 events := make([]flow.Event, 4) 57 events = append(events, unittest.EventFixture(testEventTypes[0], 0, 0, unittest.IdentifierFixture(), 0)) 58 59 accountCreateEvent := generator.GenerateAccountCreateEvent(s.T(), s.accountCreatedAddress) 60 accountCreateEvent.TransactionIndex = 1 61 events = append(events, accountCreateEvent) 62 63 accountContractAdded := generator.GenerateAccountContractEvent(s.T(), "AccountContractAdded", s.accountContractAdded) 64 accountContractAdded.TransactionIndex = 2 65 events = append(events, accountContractAdded) 66 67 accountContractUpdated := generator.GenerateAccountContractEvent(s.T(), "AccountContractUpdated", s.accountContractUpdated) 68 accountContractUpdated.TransactionIndex = 3 69 events = append(events, accountContractUpdated) 70 71 return events 72 } 73 74 // SetupTest initializes the test suite. 75 func (s *BackendAccountStatusesSuite) SetupTest() { 76 blockCount := 5 77 var err error 78 s.SetupTestSuite(blockCount) 79 80 addressGenerator := chainID.Chain().NewAddressGenerator() 81 s.accountCreatedAddress, err = addressGenerator.NextAddress() 82 require.NoError(s.T(), err) 83 s.accountContractAdded, err = addressGenerator.NextAddress() 84 require.NoError(s.T(), err) 85 s.accountContractUpdated, err = addressGenerator.NextAddress() 86 require.NoError(s.T(), err) 87 88 parent := s.rootBlock.Header 89 events := s.generateProtocolMockEvents() 90 91 for i := 0; i < blockCount; i++ { 92 block := unittest.BlockWithParentFixture(parent) 93 // update for next iteration 94 parent = block.Header 95 96 seal := unittest.BlockSealsFixture(1)[0] 97 result := unittest.ExecutionResultFixture() 98 99 chunkDatas := []*execution_data.ChunkExecutionData{ 100 unittest.ChunkExecutionDataFixture(s.T(), execution_data.DefaultMaxBlobSize/5, unittest.WithChunkEvents(events)), 101 } 102 103 execData := unittest.BlockExecutionDataFixture( 104 unittest.WithBlockExecutionDataBlockID(block.ID()), 105 unittest.WithChunkExecutionDatas(chunkDatas...), 106 ) 107 108 result.ExecutionDataID, err = s.eds.Add(context.TODO(), execData) 109 assert.NoError(s.T(), err) 110 111 s.blocks = append(s.blocks, block) 112 s.execDataMap[block.ID()] = execution_data.NewBlockExecutionDataEntity(result.ExecutionDataID, execData) 113 s.blockEvents[block.ID()] = events 114 s.blockMap[block.Header.Height] = block 115 s.sealMap[block.ID()] = seal 116 s.resultMap[seal.ResultID] = result 117 118 s.T().Logf("adding exec data for block %d %d %v => %v", i, block.Header.Height, block.ID(), result.ExecutionDataID) 119 } 120 121 s.SetupTestMocks() 122 } 123 124 // subscribeFromStartBlockIdTestCases generates test cases for subscribing from a start block ID. 125 func (s *BackendAccountStatusesSuite) subscribeFromStartBlockIdTestCases() []testType { 126 baseTests := []testType{ 127 { 128 name: "happy path - all new blocks", 129 highestBackfill: -1, // no backfill 130 startValue: s.rootBlock.ID(), 131 }, 132 { 133 name: "happy path - partial backfill", 134 highestBackfill: 2, // backfill the first 3 blocks 135 startValue: s.blocks[0].ID(), 136 }, 137 { 138 name: "happy path - complete backfill", 139 highestBackfill: len(s.blocks) - 1, // backfill all blocks 140 startValue: s.blocks[0].ID(), 141 }, 142 { 143 name: "happy path - start from root block by id", 144 highestBackfill: len(s.blocks) - 1, // backfill all blocks 145 startValue: s.rootBlock.ID(), // start from root block 146 }, 147 } 148 149 return s.generateFiltersForTestCases(baseTests) 150 } 151 152 // subscribeFromStartHeightTestCases generates test cases for subscribing from a start height. 153 func (s *BackendAccountStatusesSuite) subscribeFromStartHeightTestCases() []testType { 154 baseTests := []testType{ 155 { 156 name: "happy path - all new blocks", 157 highestBackfill: -1, // no backfill 158 startValue: s.rootBlock.Header.Height, 159 }, 160 { 161 name: "happy path - partial backfill", 162 highestBackfill: 2, // backfill the first 3 blocks 163 startValue: s.blocks[0].Header.Height, 164 }, 165 { 166 name: "happy path - complete backfill", 167 highestBackfill: len(s.blocks) - 1, // backfill all blocks 168 startValue: s.blocks[0].Header.Height, 169 }, 170 { 171 name: "happy path - start from root block by id", 172 highestBackfill: len(s.blocks) - 1, // backfill all blocks 173 startValue: s.rootBlock.Header.Height, // start from root block 174 }, 175 } 176 177 return s.generateFiltersForTestCases(baseTests) 178 } 179 180 // subscribeFromLatestTestCases generates test cases for subscribing from the latest block. 181 func (s *BackendAccountStatusesSuite) subscribeFromLatestTestCases() []testType { 182 baseTests := []testType{ 183 { 184 name: "happy path - all new blocks", 185 highestBackfill: -1, // no backfill 186 }, 187 { 188 name: "happy path - partial backfill", 189 highestBackfill: 2, // backfill the first 3 blocks 190 }, 191 { 192 name: "happy path - complete backfill", 193 highestBackfill: len(s.blocks) - 1, // backfill all blocks 194 }, 195 } 196 197 return s.generateFiltersForTestCases(baseTests) 198 } 199 200 // generateFiltersForTestCases generates variations of test cases with different event filters. 201 // 202 // This function takes an array of base testType structs and creates variations for each of them. 203 // For each base test case, it generates three variations: 204 // - All events: Includes all protocol event types filtered by the provided account address. 205 // - Some events: Includes only the first protocol event type filtered by the provided account address. 206 // - No events: Includes a custom event type "flow.AccountKeyAdded" filtered by the provided account address. 207 func (s *BackendAccountStatusesSuite) generateFiltersForTestCases(baseTests []testType) []testType { 208 // Create variations for each of the base tests 209 tests := make([]testType, 0, len(baseTests)*3) 210 var err error 211 for _, test := range baseTests { 212 t1 := test 213 t1.name = fmt.Sprintf("%s - all events", test.name) 214 t1.filters, err = state_stream.NewAccountStatusFilter( 215 state_stream.DefaultEventFilterConfig, 216 chainID.Chain(), 217 []string{string(testProtocolEventTypes[0]), string(testProtocolEventTypes[1]), string(testProtocolEventTypes[2])}, 218 []string{s.accountCreatedAddress.HexWithPrefix(), s.accountContractAdded.HexWithPrefix(), s.accountContractUpdated.HexWithPrefix()}, 219 ) 220 require.NoError(s.T(), err) 221 tests = append(tests, t1) 222 223 t2 := test 224 t2.name = fmt.Sprintf("%s - some events", test.name) 225 t2.filters, err = state_stream.NewAccountStatusFilter( 226 state_stream.DefaultEventFilterConfig, 227 chainID.Chain(), 228 []string{string(testProtocolEventTypes[0])}, 229 []string{s.accountCreatedAddress.HexWithPrefix(), s.accountContractAdded.HexWithPrefix(), s.accountContractUpdated.HexWithPrefix()}, 230 ) 231 require.NoError(s.T(), err) 232 tests = append(tests, t2) 233 234 t3 := test 235 t3.name = fmt.Sprintf("%s - no events", test.name) 236 t3.filters, err = state_stream.NewAccountStatusFilter( 237 state_stream.DefaultEventFilterConfig, 238 chainID.Chain(), 239 []string{"flow.AccountKeyAdded"}, 240 []string{s.accountCreatedAddress.HexWithPrefix(), s.accountContractAdded.HexWithPrefix(), s.accountContractUpdated.HexWithPrefix()}, 241 ) 242 require.NoError(s.T(), err) 243 tests = append(tests, t3) 244 245 t4 := test 246 t4.name = fmt.Sprintf("%s - no events, no addresses", test.name) 247 t4.filters, err = state_stream.NewAccountStatusFilter( 248 state_stream.DefaultEventFilterConfig, 249 chainID.Chain(), 250 []string{}, 251 []string{}, 252 ) 253 require.NoError(s.T(), err) 254 tests = append(tests, t4) 255 256 t5 := test 257 t5.name = fmt.Sprintf("%s - some events, no addresses", test.name) 258 t5.filters, err = state_stream.NewAccountStatusFilter( 259 state_stream.DefaultEventFilterConfig, 260 chainID.Chain(), 261 []string{"flow.AccountKeyAdded"}, 262 []string{}, 263 ) 264 require.NoError(s.T(), err) 265 tests = append(tests, t5) 266 } 267 268 return tests 269 } 270 271 // subscribeToAccountStatuses runs subscription tests for account statuses. 272 // 273 // This function takes a subscribeFn function, which is a subscription function for account statuses, 274 // and an array of testType structs representing the test cases. 275 // It iterates over each test case and sets up the necessary context and cancellation for the subscription. 276 // For each test case, it simulates backfill blocks and verifies the expected account events for each block. 277 // It also ensures that the subscription shuts down gracefully after completing the test cases. 278 func (s *BackendAccountStatusesSuite) subscribeToAccountStatuses( 279 subscribeFn func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription, 280 tests []testType, 281 ) { 282 ctx, cancel := context.WithCancel(context.Background()) 283 defer cancel() 284 285 // Iterate over each test case 286 for _, test := range tests { 287 s.Run(test.name, func() { 288 s.T().Logf("len(s.execDataMap) %d", len(s.execDataMap)) 289 290 // Add "backfill" block - blocks that are already in the database before the test starts 291 // This simulates a subscription on a past block 292 if test.highestBackfill > 0 { 293 s.highestBlockHeader = s.blocks[test.highestBackfill].Header 294 } 295 296 // Set up subscription context and cancellation 297 subCtx, subCancel := context.WithCancel(ctx) 298 299 sub := subscribeFn(subCtx, test.startValue, test.filters) 300 301 // Loop over all the blocks 302 for i, b := range s.blocks { 303 s.T().Logf("checking block %d %v", i, b.ID()) 304 305 // Simulate new exec data received. 306 // Exec data for all blocks with index <= highestBackfill were already received 307 if i > test.highestBackfill { 308 s.highestBlockHeader = b.Header 309 310 s.broadcaster.Publish() 311 } 312 313 expectedEvents := map[string]flow.EventsList{} 314 for _, event := range s.blockEvents[b.ID()] { 315 if test.filters.Match(event) { 316 var address string 317 switch event.Type { 318 case state_stream.CoreEventAccountCreated: 319 address = s.accountCreatedAddress.HexWithPrefix() 320 case state_stream.CoreEventAccountContractAdded: 321 address = s.accountContractAdded.HexWithPrefix() 322 case state_stream.CoreEventAccountContractUpdated: 323 address = s.accountContractUpdated.HexWithPrefix() 324 } 325 expectedEvents[address] = append(expectedEvents[address], event) 326 } 327 } 328 329 // Consume execution data from subscription 330 unittest.RequireReturnsBefore(s.T(), func() { 331 v, ok := <-sub.Channel() 332 require.True(s.T(), ok, "channel closed while waiting for exec data for block %d %v: err: %v", b.Header.Height, b.ID(), sub.Err()) 333 334 resp, ok := v.(*AccountStatusesResponse) 335 require.True(s.T(), ok, "unexpected response type: %T", v) 336 337 assert.Equal(s.T(), b.Header.ID(), resp.BlockID) 338 assert.Equal(s.T(), b.Header.Height, resp.Height) 339 assert.Equal(s.T(), expectedEvents, resp.AccountEvents) 340 }, 60*time.Second, fmt.Sprintf("timed out waiting for exec data for block %d %v", b.Header.Height, b.ID())) 341 } 342 343 // Make sure there are no new messages waiting. The channel should be opened with nothing waiting 344 unittest.RequireNeverReturnBefore(s.T(), func() { 345 <-sub.Channel() 346 }, 100*time.Millisecond, "timed out waiting for subscription to shutdown") 347 348 // Stop the subscription 349 subCancel() 350 351 // Ensure subscription shuts down gracefully 352 unittest.RequireReturnsBefore(s.T(), func() { 353 v, ok := <-sub.Channel() 354 assert.Nil(s.T(), v) 355 assert.False(s.T(), ok) 356 assert.ErrorIs(s.T(), sub.Err(), context.Canceled) 357 }, 100*time.Millisecond, "timed out waiting for subscription to shutdown") 358 }) 359 } 360 } 361 362 // TestSubscribeAccountStatusesFromStartBlockID tests the SubscribeAccountStatusesFromStartBlockID method. 363 func (s *BackendAccountStatusesSuite) TestSubscribeAccountStatusesFromStartBlockID() { 364 s.executionDataTracker.On( 365 "GetStartHeightFromBlockID", 366 mock.AnythingOfType("flow.Identifier"), 367 ).Return(func(startBlockID flow.Identifier) (uint64, error) { 368 return s.executionDataTrackerReal.GetStartHeightFromBlockID(startBlockID) 369 }, nil) 370 371 call := func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription { 372 return s.backend.SubscribeAccountStatusesFromStartBlockID(ctx, startValue.(flow.Identifier), filter) 373 } 374 375 s.subscribeToAccountStatuses(call, s.subscribeFromStartBlockIdTestCases()) 376 } 377 378 // TestSubscribeAccountStatusesFromStartHeight tests the SubscribeAccountStatusesFromStartHeight method. 379 func (s *BackendAccountStatusesSuite) TestSubscribeAccountStatusesFromStartHeight() { 380 s.executionDataTracker.On( 381 "GetStartHeightFromHeight", 382 mock.AnythingOfType("uint64"), 383 ).Return(func(startHeight uint64) (uint64, error) { 384 return s.executionDataTrackerReal.GetStartHeightFromHeight(startHeight) 385 }, nil) 386 387 call := func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription { 388 return s.backend.SubscribeAccountStatusesFromStartHeight(ctx, startValue.(uint64), filter) 389 } 390 391 s.subscribeToAccountStatuses(call, s.subscribeFromStartHeightTestCases()) 392 } 393 394 // TestSubscribeAccountStatusesFromLatestBlock tests the SubscribeAccountStatusesFromLatestBlock method. 395 func (s *BackendAccountStatusesSuite) TestSubscribeAccountStatusesFromLatestBlock() { 396 s.executionDataTracker.On( 397 "GetStartHeightFromLatest", 398 mock.Anything, 399 ).Return(func(ctx context.Context) (uint64, error) { 400 return s.executionDataTrackerReal.GetStartHeightFromLatest(ctx) 401 }, nil) 402 403 call := func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription { 404 return s.backend.SubscribeAccountStatusesFromLatestBlock(ctx, filter) 405 } 406 407 s.subscribeToAccountStatuses(call, s.subscribeFromLatestTestCases()) 408 } 409 410 // TestSubscribeAccountStatusesHandlesErrors tests handling of expected errors in the SubscribeAccountStatuses. 411 func (s *BackendExecutionDataSuite) TestSubscribeAccountStatusesHandlesErrors() { 412 ctx, cancel := context.WithCancel(context.Background()) 413 defer cancel() 414 415 // mock block tracker for SubscribeBlocksFromStartBlockID 416 s.executionDataTracker.On( 417 "GetStartHeightFromBlockID", 418 mock.AnythingOfType("flow.Identifier"), 419 ).Return(func(startBlockID flow.Identifier) (uint64, error) { 420 return s.executionDataTrackerReal.GetStartHeightFromBlockID(startBlockID) 421 }, nil) 422 423 s.Run("returns error for unindexed start blockID", func() { 424 subCtx, subCancel := context.WithCancel(ctx) 425 defer subCancel() 426 427 sub := s.backend.SubscribeAccountStatusesFromStartBlockID(subCtx, unittest.IdentifierFixture(), state_stream.AccountStatusFilter{}) 428 assert.Equal(s.T(), codes.NotFound, status.Code(sub.Err()), "expected NotFound, got %v: %v", status.Code(sub.Err()).String(), sub.Err()) 429 }) 430 431 s.executionDataTracker.On( 432 "GetStartHeightFromHeight", 433 mock.AnythingOfType("uint64"), 434 ).Return(func(startHeight uint64) (uint64, error) { 435 return s.executionDataTrackerReal.GetStartHeightFromHeight(startHeight) 436 }, nil) 437 438 s.Run("returns error for start height before root height", func() { 439 subCtx, subCancel := context.WithCancel(ctx) 440 defer subCancel() 441 442 sub := s.backend.SubscribeAccountStatusesFromStartHeight(subCtx, s.rootBlock.Header.Height-1, state_stream.AccountStatusFilter{}) 443 assert.Equal(s.T(), codes.InvalidArgument, status.Code(sub.Err()), "expected InvalidArgument, got %v: %v", status.Code(sub.Err()).String(), sub.Err()) 444 }) 445 446 // make sure we're starting with a fresh cache 447 s.execDataHeroCache.Clear() 448 449 s.Run("returns error for unindexed start height", func() { 450 subCtx, subCancel := context.WithCancel(ctx) 451 defer subCancel() 452 453 sub := s.backend.SubscribeAccountStatusesFromStartHeight(subCtx, s.blocks[len(s.blocks)-1].Header.Height+10, state_stream.AccountStatusFilter{}) 454 assert.Equal(s.T(), codes.NotFound, status.Code(sub.Err()), "expected NotFound, got %v: %v", status.Code(sub.Err()).String(), sub.Err()) 455 }) 456 }