integration tests connectivity suite

This commit is contained in:
pragmaxim
2026-01-22 11:04:15 +01:00
parent 8542377fa3
commit a2274c9bbf
4 changed files with 194 additions and 86 deletions

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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")

View File

@@ -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"]
}
}