300 lines
9.3 KiB
Go
300 lines
9.3 KiB
Go
|
package chain
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"encoding/json"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"github.com/status-im/status-go/healthmanager"
|
||
|
"github.com/status-im/status-go/healthmanager/rpcstatus"
|
||
|
mockEthclient "github.com/status-im/status-go/rpc/chain/ethclient/mock/client/ethclient"
|
||
|
|
||
|
"github.com/stretchr/testify/require"
|
||
|
"github.com/stretchr/testify/suite"
|
||
|
|
||
|
"github.com/ethereum/go-ethereum/common"
|
||
|
"github.com/ethereum/go-ethereum/core/types"
|
||
|
|
||
|
"go.uber.org/mock/gomock"
|
||
|
|
||
|
"github.com/status-im/status-go/rpc/chain/ethclient"
|
||
|
)
|
||
|
|
||
|
type BlockchainHealthManagerSuite struct {
|
||
|
suite.Suite
|
||
|
blockchainHealthManager *healthmanager.BlockchainHealthManager
|
||
|
mockProviders map[uint64]*healthmanager.ProvidersHealthManager
|
||
|
mockEthClients map[uint64]*mockEthclient.MockRPSLimitedEthClientInterface
|
||
|
clients map[uint64]*ClientWithFallback
|
||
|
mockCtrl *gomock.Controller
|
||
|
}
|
||
|
|
||
|
func (s *BlockchainHealthManagerSuite) SetupTest() {
|
||
|
s.blockchainHealthManager = healthmanager.NewBlockchainHealthManager()
|
||
|
s.mockProviders = make(map[uint64]*healthmanager.ProvidersHealthManager)
|
||
|
s.mockEthClients = make(map[uint64]*mockEthclient.MockRPSLimitedEthClientInterface)
|
||
|
s.clients = make(map[uint64]*ClientWithFallback)
|
||
|
s.mockCtrl = gomock.NewController(s.T())
|
||
|
}
|
||
|
|
||
|
func (s *BlockchainHealthManagerSuite) TearDownTest() {
|
||
|
s.blockchainHealthManager.Stop()
|
||
|
s.mockCtrl.Finish()
|
||
|
}
|
||
|
|
||
|
func (s *BlockchainHealthManagerSuite) setupClients(chainIDs []uint64) {
|
||
|
ctx := context.Background()
|
||
|
|
||
|
for _, chainID := range chainIDs {
|
||
|
mockEthClient := mockEthclient.NewMockRPSLimitedEthClientInterface(s.mockCtrl)
|
||
|
mockEthClient.EXPECT().GetName().AnyTimes().Return(fmt.Sprintf("test_client_chain_%d", chainID))
|
||
|
mockEthClient.EXPECT().GetLimiter().AnyTimes().Return(nil)
|
||
|
|
||
|
phm := healthmanager.NewProvidersHealthManager(chainID)
|
||
|
client := NewClient([]ethclient.RPSLimitedEthClientInterface{mockEthClient}, chainID, phm)
|
||
|
|
||
|
err := s.blockchainHealthManager.RegisterProvidersHealthManager(ctx, phm)
|
||
|
require.NoError(s.T(), err)
|
||
|
|
||
|
s.mockProviders[chainID] = phm
|
||
|
s.mockEthClients[chainID] = mockEthClient
|
||
|
s.clients[chainID] = client
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *BlockchainHealthManagerSuite) simulateChainStatus(chainID uint64, up bool) {
|
||
|
client, exists := s.clients[chainID]
|
||
|
require.True(s.T(), exists, "Client for chainID %d not found", chainID)
|
||
|
|
||
|
mockEthClient := s.mockEthClients[chainID]
|
||
|
ctx := context.Background()
|
||
|
hash := common.HexToHash("0x1234")
|
||
|
|
||
|
if up {
|
||
|
block := &types.Block{}
|
||
|
mockEthClient.EXPECT().BlockByHash(ctx, hash).Return(block, nil).Times(1)
|
||
|
_, err := client.BlockByHash(ctx, hash)
|
||
|
require.NoError(s.T(), err)
|
||
|
} else {
|
||
|
mockEthClient.EXPECT().BlockByHash(ctx, hash).Return(nil, errors.New("no such host")).Times(1)
|
||
|
_, err := client.BlockByHash(ctx, hash)
|
||
|
require.Error(s.T(), err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *BlockchainHealthManagerSuite) waitForStatus(statusCh chan struct{}, expectedStatus rpcstatus.StatusType) {
|
||
|
timeout := time.After(2 * time.Second)
|
||
|
for {
|
||
|
select {
|
||
|
case <-statusCh:
|
||
|
status := s.blockchainHealthManager.Status()
|
||
|
if status.Status == expectedStatus {
|
||
|
return
|
||
|
}
|
||
|
case <-timeout:
|
||
|
s.T().Errorf("Did not receive expected blockchain status update in time")
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *BlockchainHealthManagerSuite) TestAllChainsUp() {
|
||
|
s.setupClients([]uint64{1, 2, 3})
|
||
|
|
||
|
statusCh := s.blockchainHealthManager.Subscribe()
|
||
|
defer s.blockchainHealthManager.Unsubscribe(statusCh)
|
||
|
|
||
|
s.simulateChainStatus(1, true)
|
||
|
s.simulateChainStatus(2, true)
|
||
|
s.simulateChainStatus(3, true)
|
||
|
|
||
|
s.waitForStatus(statusCh, rpcstatus.StatusUp)
|
||
|
}
|
||
|
|
||
|
func (s *BlockchainHealthManagerSuite) TestSomeChainsDown() {
|
||
|
s.setupClients([]uint64{1, 2, 3})
|
||
|
|
||
|
statusCh := s.blockchainHealthManager.Subscribe()
|
||
|
defer s.blockchainHealthManager.Unsubscribe(statusCh)
|
||
|
|
||
|
s.simulateChainStatus(1, true)
|
||
|
s.simulateChainStatus(2, false)
|
||
|
s.simulateChainStatus(3, true)
|
||
|
|
||
|
s.waitForStatus(statusCh, rpcstatus.StatusUp)
|
||
|
}
|
||
|
|
||
|
func (s *BlockchainHealthManagerSuite) TestAllChainsDown() {
|
||
|
s.setupClients([]uint64{1, 2})
|
||
|
|
||
|
statusCh := s.blockchainHealthManager.Subscribe()
|
||
|
defer s.blockchainHealthManager.Unsubscribe(statusCh)
|
||
|
|
||
|
s.simulateChainStatus(1, false)
|
||
|
s.simulateChainStatus(2, false)
|
||
|
|
||
|
s.waitForStatus(statusCh, rpcstatus.StatusDown)
|
||
|
}
|
||
|
|
||
|
func (s *BlockchainHealthManagerSuite) TestChainStatusChanges() {
|
||
|
s.setupClients([]uint64{1, 2})
|
||
|
|
||
|
statusCh := s.blockchainHealthManager.Subscribe()
|
||
|
defer s.blockchainHealthManager.Unsubscribe(statusCh)
|
||
|
|
||
|
s.simulateChainStatus(1, false)
|
||
|
s.simulateChainStatus(2, false)
|
||
|
s.waitForStatus(statusCh, rpcstatus.StatusDown)
|
||
|
|
||
|
s.simulateChainStatus(1, true)
|
||
|
s.waitForStatus(statusCh, rpcstatus.StatusUp)
|
||
|
}
|
||
|
|
||
|
func (s *BlockchainHealthManagerSuite) TestGetFullStatus() {
|
||
|
// Setup clients for chain IDs 1 and 2
|
||
|
s.setupClients([]uint64{1, 2})
|
||
|
|
||
|
// Subscribe to blockchain status updates
|
||
|
statusCh := s.blockchainHealthManager.Subscribe()
|
||
|
defer s.blockchainHealthManager.Unsubscribe(statusCh)
|
||
|
|
||
|
// Simulate provider statuses for chain 1
|
||
|
providerCallStatusesChain1 := []rpcstatus.RpcProviderCallStatus{
|
||
|
{
|
||
|
Name: "provider1_chain1",
|
||
|
Timestamp: time.Now(),
|
||
|
Err: nil, // Up
|
||
|
},
|
||
|
{
|
||
|
Name: "provider2_chain1",
|
||
|
Timestamp: time.Now(),
|
||
|
Err: errors.New("connection error"), // Down
|
||
|
},
|
||
|
}
|
||
|
ctx := context.Background()
|
||
|
s.mockProviders[1].Update(ctx, providerCallStatusesChain1)
|
||
|
|
||
|
// Simulate provider statuses for chain 2
|
||
|
providerCallStatusesChain2 := []rpcstatus.RpcProviderCallStatus{
|
||
|
{
|
||
|
Name: "provider1_chain2",
|
||
|
Timestamp: time.Now(),
|
||
|
Err: nil, // Up
|
||
|
},
|
||
|
{
|
||
|
Name: "provider2_chain2",
|
||
|
Timestamp: time.Now(),
|
||
|
Err: nil, // Up
|
||
|
},
|
||
|
}
|
||
|
s.mockProviders[2].Update(ctx, providerCallStatusesChain2)
|
||
|
|
||
|
// Wait for status event to be triggered before getting full status
|
||
|
s.waitForStatus(statusCh, rpcstatus.StatusUp)
|
||
|
|
||
|
// Get the full status from the BlockchainHealthManager
|
||
|
fullStatus := s.blockchainHealthManager.GetFullStatus()
|
||
|
|
||
|
// Assert overall blockchain status
|
||
|
require.Equal(s.T(), rpcstatus.StatusUp, fullStatus.Status.Status)
|
||
|
|
||
|
// Assert provider statuses per chain
|
||
|
require.Contains(s.T(), fullStatus.StatusPerChainPerProvider, uint64(1))
|
||
|
require.Contains(s.T(), fullStatus.StatusPerChainPerProvider, uint64(2))
|
||
|
|
||
|
// Provider statuses for chain 1
|
||
|
providerStatusesChain1 := fullStatus.StatusPerChainPerProvider[1]
|
||
|
require.Contains(s.T(), providerStatusesChain1, "provider1_chain1")
|
||
|
require.Contains(s.T(), providerStatusesChain1, "provider2_chain1")
|
||
|
|
||
|
provider1Chain1Status := providerStatusesChain1["provider1_chain1"]
|
||
|
require.Equal(s.T(), rpcstatus.StatusUp, provider1Chain1Status.Status)
|
||
|
|
||
|
provider2Chain1Status := providerStatusesChain1["provider2_chain1"]
|
||
|
require.Equal(s.T(), rpcstatus.StatusDown, provider2Chain1Status.Status)
|
||
|
|
||
|
// Provider statuses for chain 2
|
||
|
providerStatusesChain2 := fullStatus.StatusPerChainPerProvider[2]
|
||
|
require.Contains(s.T(), providerStatusesChain2, "provider1_chain2")
|
||
|
require.Contains(s.T(), providerStatusesChain2, "provider2_chain2")
|
||
|
|
||
|
provider1Chain2Status := providerStatusesChain2["provider1_chain2"]
|
||
|
require.Equal(s.T(), rpcstatus.StatusUp, provider1Chain2Status.Status)
|
||
|
|
||
|
provider2Chain2Status := providerStatusesChain2["provider2_chain2"]
|
||
|
require.Equal(s.T(), rpcstatus.StatusUp, provider2Chain2Status.Status)
|
||
|
|
||
|
// Serialization to JSON works without errors
|
||
|
jsonData, err := json.MarshalIndent(fullStatus, "", " ")
|
||
|
require.NoError(s.T(), err)
|
||
|
require.NotEmpty(s.T(), jsonData)
|
||
|
}
|
||
|
|
||
|
func (s *BlockchainHealthManagerSuite) TestGetShortStatus() {
|
||
|
// Setup clients for chain IDs 1 and 2
|
||
|
s.setupClients([]uint64{1, 2})
|
||
|
|
||
|
// Subscribe to blockchain status updates
|
||
|
statusCh := s.blockchainHealthManager.Subscribe()
|
||
|
defer s.blockchainHealthManager.Unsubscribe(statusCh)
|
||
|
|
||
|
// Simulate provider statuses for chain 1
|
||
|
providerCallStatusesChain1 := []rpcstatus.RpcProviderCallStatus{
|
||
|
{
|
||
|
Name: "provider1_chain1",
|
||
|
Timestamp: time.Now(),
|
||
|
Err: nil, // Up
|
||
|
},
|
||
|
{
|
||
|
Name: "provider2_chain1",
|
||
|
Timestamp: time.Now(),
|
||
|
Err: errors.New("connection error"), // Down
|
||
|
},
|
||
|
}
|
||
|
ctx := context.Background()
|
||
|
s.mockProviders[1].Update(ctx, providerCallStatusesChain1)
|
||
|
|
||
|
// Simulate provider statuses for chain 2
|
||
|
providerCallStatusesChain2 := []rpcstatus.RpcProviderCallStatus{
|
||
|
{
|
||
|
Name: "provider1_chain2",
|
||
|
Timestamp: time.Now(),
|
||
|
Err: nil, // Up
|
||
|
},
|
||
|
{
|
||
|
Name: "provider2_chain2",
|
||
|
Timestamp: time.Now(),
|
||
|
Err: nil, // Up
|
||
|
},
|
||
|
}
|
||
|
s.mockProviders[2].Update(ctx, providerCallStatusesChain2)
|
||
|
|
||
|
// Wait for status event to be triggered before getting short status
|
||
|
s.waitForStatus(statusCh, rpcstatus.StatusUp)
|
||
|
|
||
|
// Get the short status from the BlockchainHealthManager
|
||
|
shortStatus := s.blockchainHealthManager.GetStatusPerChain()
|
||
|
|
||
|
// Assert overall blockchain status
|
||
|
require.Equal(s.T(), rpcstatus.StatusUp, shortStatus.Status.Status)
|
||
|
|
||
|
// Assert chain statuses
|
||
|
require.Contains(s.T(), shortStatus.StatusPerChain, uint64(1))
|
||
|
require.Contains(s.T(), shortStatus.StatusPerChain, uint64(2))
|
||
|
|
||
|
require.Equal(s.T(), rpcstatus.StatusUp, shortStatus.StatusPerChain[1].Status)
|
||
|
require.Equal(s.T(), rpcstatus.StatusUp, shortStatus.StatusPerChain[2].Status)
|
||
|
|
||
|
// Serialization to JSON works without errors
|
||
|
jsonData, err := json.MarshalIndent(shortStatus, "", " ")
|
||
|
require.NoError(s.T(), err)
|
||
|
require.NotEmpty(s.T(), jsonData)
|
||
|
}
|
||
|
|
||
|
func TestBlockchainHealthManagerSuite(t *testing.T) {
|
||
|
suite.Run(t, new(BlockchainHealthManagerSuite))
|
||
|
}
|