diff --git a/tests/connectivity/connectivity.go b/tests/connectivity/connectivity.go new file mode 100644 index 00000000..e310fcd0 --- /dev/null +++ b/tests/connectivity/connectivity.go @@ -0,0 +1,168 @@ +//go:build integration + +package connectivity + +import ( + "context" + "encoding/json" + "errors" + "testing" + "time" + + "github.com/ethereum/go-ethereum/rpc" + "github.com/trezor/blockbook/bchain" + "github.com/trezor/blockbook/bchain/coins" +) + +const connectivityTimeout = 10 * time.Second + +type connectivityCfg struct { + CoinName string `json:"coin_name"` + RpcUrl string `json:"rpc_url"` + RpcUrlWs string `json:"rpc_url_ws"` + RpcUser string `json:"rpc_user"` + RpcPass string `json:"rpc_pass"` +} + +// IntegrationTest runs connectivity checks for the requested modes (e.g., ["http","ws"]). +// HTTP checks verify the backend responds (UTXO uses getblockchaininfo, EVM uses web3_clientVersion). +// WS checks verify web3_clientVersion and a newHeads subscription over the WS endpoint. +func IntegrationTest(t *testing.T, coin string, _ bchain.BlockChain, _ bchain.Mempool, testConfig json.RawMessage) { + t.Helper() + + modes, err := parseConnectivityModes(testConfig) + if err != nil { + t.Fatalf("invalid connectivity config for %s: %v", coin, err) + } + + for _, mode := range modes { + switch mode { + case "http": + HTTPIntegrationTest(t, coin, nil, nil, nil) + case "ws": + WSIntegrationTest(t, coin, nil, nil, nil) + default: + t.Fatalf("unsupported connectivity mode %q for %s", mode, coin) + } + } +} + +func parseConnectivityModes(testConfig json.RawMessage) ([]string, error) { + var modes []string + if err := json.Unmarshal(testConfig, &modes); err != nil { + return nil, err + } + if len(modes) == 0 { + return nil, errors.New("empty connectivity list") + } + return modes, nil +} + +func HTTPIntegrationTest(t *testing.T, coin string, _ bchain.BlockChain, _ bchain.Mempool, _ json.RawMessage) { + t.Helper() + + rawCfg, cfg := loadConnectivityCfg(t, coin) + if cfg.RpcUrl == "" { + t.Fatalf("empty rpc_url for %s", coin) + } + + if isUTXO(cfg) { + if cfg.CoinName == "" { + t.Fatalf("empty coin_name for %s", coin) + } + factory, ok := coins.BlockChainFactories[cfg.CoinName] + if !ok { + t.Fatalf("blockchain factory not found for %s", cfg.CoinName) + } + chain, err := factory(rawCfg, func(bchain.NotificationType) {}) + if err != nil { + t.Fatalf("init chain %s: %v", cfg.CoinName, err) + } + if _, err := chain.GetChainInfo(); err != nil { + t.Fatalf("GetChainInfo %s: %v", cfg.CoinName, err) + } + return + } + + evmHTTPConnectivity(t, cfg.RpcUrl) +} + +func WSIntegrationTest(t *testing.T, coin string, _ bchain.BlockChain, _ bchain.Mempool, _ json.RawMessage) { + t.Helper() + + _, cfg := loadConnectivityCfg(t, coin) + if cfg.RpcUrlWs == "" { + t.Fatalf("empty rpc_url_ws for %s", coin) + } + + evmWSConnectivity(t, cfg.RpcUrlWs) +} + +func loadConnectivityCfg(t *testing.T, coin string) (json.RawMessage, connectivityCfg) { + t.Helper() + + rawCfg, err := bchain.LoadBlockchainCfgRaw(coin) + if err != nil { + t.Fatalf("load blockchain config for %s: %v", coin, err) + } + var cfg connectivityCfg + if err := json.Unmarshal(rawCfg, &cfg); err != nil { + t.Fatalf("unmarshal blockchain config for %s: %v", coin, err) + } + return rawCfg, cfg +} + +func isUTXO(cfg connectivityCfg) bool { + return cfg.RpcUser != "" || cfg.RpcPass != "" +} + +func evmHTTPConnectivity(t *testing.T, httpURL string) { + t.Helper() + + rpcClient, err := rpc.DialOptions(context.Background(), httpURL) + if err != nil { + t.Fatalf("dial rpc_url %s: %v", httpURL, err) + } + defer rpcClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), connectivityTimeout) + defer cancel() + + var version string + if err := rpcClient.CallContext(ctx, &version, "web3_clientVersion"); err != nil { + t.Fatalf("CallContext web3_clientVersion failed: %v", err) + } + if version == "" { + t.Fatalf("empty web3_clientVersion") + } +} + +func evmWSConnectivity(t *testing.T, wsURL string) { + t.Helper() + + rpcClient, err := rpc.DialOptions(context.Background(), wsURL, rpc.WithWebsocketMessageSizeLimit(0)) + if err != nil { + t.Fatalf("dial rpc_url_ws %s: %v", wsURL, err) + } + defer rpcClient.Close() + + ctx, cancel := context.WithTimeout(context.Background(), connectivityTimeout) + defer cancel() + + var version string + if err := rpcClient.CallContext(ctx, &version, "web3_clientVersion"); err != nil { + t.Fatalf("CallContext web3_clientVersion failed: %v", err) + } + if version == "" { + t.Fatalf("empty web3_clientVersion") + } + + subCtx, subCancel := context.WithTimeout(context.Background(), connectivityTimeout) + defer subCancel() + + sub, err := rpcClient.EthSubscribe(subCtx, make(chan interface{}, 1), "newHeads") + if err != nil { + t.Fatalf("EthSubscribe newHeads failed: %v", err) + } + sub.Unsubscribe() +} diff --git a/tests/evm/evm_rpc_clients.go b/tests/evm/evm_rpc_clients.go deleted file mode 100644 index fd687c38..00000000 --- a/tests/evm/evm_rpc_clients.go +++ /dev/null @@ -1,68 +0,0 @@ -//go:build integration - -package evm - -import ( - "context" - "encoding/json" - "testing" - "time" - - "github.com/trezor/blockbook/bchain" - "github.com/trezor/blockbook/bchain/coins/avalanche" - "github.com/trezor/blockbook/bchain/coins/eth" -) - -type openRPCFunc func(string, string) (bchain.EVMRPCClient, bchain.EVMClient, error) - -var openRPCOverrides = map[string]openRPCFunc{ - "avalanche": avalanche.OpenRPC, -} - -func IntegrationTest(t *testing.T, coin string, _ bchain.BlockChain, _ bchain.Mempool, _ json.RawMessage) { - t.Helper() - - openRPC := eth.OpenRPC - if override, ok := openRPCOverrides[coin]; ok { - openRPC = override - } - - runEVMRPCClientIntegrationTest(t, coin, openRPC) -} - -func runEVMRPCClientIntegrationTest(t *testing.T, coinAlias string, openRPC openRPCFunc) { - t.Helper() - - cfg := bchain.LoadBlockchainCfg(t, coinAlias) - if cfg.RpcUrl == "" { - t.Fatalf("empty rpc_url for %s", coinAlias) - } - if cfg.RpcUrlWs == "" { - t.Fatalf("empty rpc_url_ws for %s", coinAlias) - } - - rpcClient, _, err := openRPC(cfg.RpcUrl, cfg.RpcUrlWs) - if err != nil { - t.Fatalf("open rpc clients: %v", err) - } - defer rpcClient.Close() - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - var version string - if err := rpcClient.CallContext(ctx, &version, "web3_clientVersion"); err != nil { - t.Fatalf("CallContext web3_clientVersion failed: %v", err) - } - if version == "" { - t.Fatalf("empty web3_clientVersion") - } - - subCtx, subCancel := context.WithTimeout(context.Background(), 10*time.Second) - defer subCancel() - sub, err := rpcClient.EthSubscribe(subCtx, make(chan interface{}, 1), "newHeads") - if err != nil { - t.Fatalf("EthSubscribe newHeads failed: %v", err) - } - sub.Unsubscribe() -} diff --git a/tests/integration.go b/tests/integration.go index ef5aed83..3c932046 100644 --- a/tests/integration.go +++ b/tests/integration.go @@ -18,7 +18,7 @@ import ( "github.com/martinboehm/btcutil/chaincfg" "github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/bchain/coins" - "github.com/trezor/blockbook/tests/evm" + "github.com/trezor/blockbook/tests/connectivity" "github.com/trezor/blockbook/tests/rpc" synctests "github.com/trezor/blockbook/tests/sync" ) @@ -30,10 +30,14 @@ type integrationTest struct { requiresChain bool } +// integrationTests maps test group names from tests.json to their handlers. +// "connectivity" performs lightweight backend reachability checks. +// "rpc" runs per-coin RPC fixtures against a fully initialized chain. +// "sync" exercises block connection/rollback logic and needs a live backend + chain init. var integrationTests = map[string]integrationTest{ - "rpc": {fn: rpc.IntegrationTest, requiresChain: true}, - "sync": {fn: synctests.IntegrationTest, requiresChain: true}, - "evm_connectivity": {fn: evm.IntegrationTest, requiresChain: false}, + "rpc": {fn: rpc.IntegrationTest, requiresChain: true}, + "sync": {fn: synctests.IntegrationTest, requiresChain: true}, + "connectivity": {fn: connectivity.IntegrationTest, requiresChain: false}, } var notConnectedError = errors.New("Not connected to backend server") diff --git a/tests/tests.json b/tests/tests.json index c441a44f..981c28d8 100644 --- a/tests/tests.json +++ b/tests/tests.json @@ -1,9 +1,10 @@ { "avalanche": { - "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"], - "evm_connectivity": true + "connectivity": ["http", "ws"], + "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"] }, "bcash": { + "connectivity": ["http"], "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync", "EstimateFee", "GetBestBlockHash", "GetBestBlockHeight", "GetBlockHeader"], "sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"] @@ -22,16 +23,19 @@ "sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"] }, "bitcoin": { + "connectivity": ["http"], "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync", "EstimateSmartFee", "EstimateFee", "GetBestBlockHash", "GetBestBlockHeight", "GetBlockHeader"], "sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"] }, "bitcoin_testnet": { + "connectivity": ["http"], "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync", "EstimateSmartFee", "EstimateFee", "GetBestBlockHash", "GetBestBlockHeight", "GetBlockHeader"], "sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"] }, "bitcoin_testnet4": { + "connectivity": ["http"], "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "GetTransactionForMempool", "MempoolSync", "EstimateSmartFee", "EstimateFee", "GetBestBlockHash", "GetBestBlockHeight", "GetBlockHeader"], "sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"] @@ -52,8 +56,8 @@ "sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"] }, "bsc": { - "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"], - "evm_connectivity": true + "connectivity": ["http", "ws"], + "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"] }, "bsc_archive": { "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader"] @@ -253,23 +257,23 @@ "sync": ["ConnectBlocksParallel", "ConnectBlocks", "HandleFork"] }, "arbitrum": { - "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"], - "evm_connectivity": true + "connectivity": ["http", "ws"], + "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"] }, "base": { - "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"], - "evm_connectivity": true + "connectivity": ["http", "ws"], + "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"] }, "ethereum": { - "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"], - "evm_connectivity": true + "connectivity": ["http", "ws"], + "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"] }, "optimism": { - "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"], - "evm_connectivity": true + "connectivity": ["http", "ws"], + "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"] }, "polygon": { - "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"], - "evm_connectivity": true + "connectivity": ["http", "ws"], + "rpc": ["GetBlock", "GetBlockHash", "GetTransaction", "EstimateFee", "GetBlockHeader", "EthCallBatch"] } }