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