Files
blockbook/tests/connectivity/connectivity.go
2026-01-22 11:04:15 +01:00

169 lines
4.4 KiB
Go

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