//go:build integration package tests import ( "encoding/json" "errors" "fmt" "io/ioutil" "net" "reflect" "sort" "strings" "sync" "testing" "time" "github.com/martinboehm/btcutil/chaincfg" "github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/bchain/coins" "github.com/trezor/blockbook/tests/connectivity" "github.com/trezor/blockbook/tests/rpc" synctests "github.com/trezor/blockbook/tests/sync" ) type TestFunc func(t *testing.T, coin string, chain bchain.BlockChain, mempool bchain.Mempool, testConfig json.RawMessage) type integrationTest struct { fn TestFunc 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}, "connectivity": {fn: connectivity.IntegrationTest, requiresChain: false}, } var notConnectedError = errors.New("Not connected to backend server") func runIntegrationTests(t *testing.T) { tests, err := loadTests("tests.json") if err != nil { t.Fatal(err) } keys := make([]string, 0, len(tests)) for k, cfg := range tests { if !hasConnectivity(cfg) { continue } keys = append(keys, k) } sort.Strings(keys) for _, coin := range keys { cfg := tests[coin] name := getMatchableName(coin) t.Run(name, func(t *testing.T) { runTests(t, coin, cfg) }) } } func loadTests(path string) (map[string]map[string]json.RawMessage, error) { b, err := ioutil.ReadFile(path) if err != nil { return nil, err } v := make(map[string]map[string]json.RawMessage) err = json.Unmarshal(b, &v) return v, err } func getMatchableName(coin string) string { if idx := strings.Index(coin, "_testnet"); idx != -1 { return coin[:idx] + "=test" } else { return coin + "=main" } } func runTests(t *testing.T, coin string, cfg map[string]json.RawMessage) { if cfg == nil || len(cfg) == 0 { t.Skip("No tests to run") } defer chaincfg.ResetParams() var ( bc bchain.BlockChain m bchain.Mempool initOnce sync.Once initErr error ) needsMempool := requiresMempool(cfg) ensureChain := func(t *testing.T) { t.Helper() initOnce.Do(func() { bc, m, initErr = makeBlockChain(coin, needsMempool) }) if initErr != nil { if initErr == notConnectedError { t.Fatal(initErr) } t.Fatalf("Cannot init blockchain: %s", initErr) } } for test, c := range cfg { if def, found := integrationTests[test]; found { t.Run(test, func(t *testing.T) { if def.requiresChain { ensureChain(t) } def.fn(t, coin, bc, m, c) }) } else { t.Errorf("Test not found: %s", test) } } } func makeBlockChain(coin string, needsMempool bool) (bchain.BlockChain, bchain.Mempool, error) { cfg, err := bchain.LoadBlockchainCfgRaw(coin) if err != nil { return nil, nil, err } coinName, err := getName(cfg) if err != nil { return nil, nil, err } return initBlockChain(coinName, cfg, needsMempool) } func getName(raw json.RawMessage) (string, error) { var cfg map[string]interface{} err := json.Unmarshal(raw, &cfg) if err != nil { return "", err } if n, found := cfg["coin_name"]; found { switch n := n.(type) { case string: return n, nil default: return "", fmt.Errorf("Unexpected type of field `name`: %s", reflect.TypeOf(n)) } } else { return "", errors.New("Missing field `name`") } } func initBlockChain(coinName string, cfg json.RawMessage, initMempool bool) (bchain.BlockChain, bchain.Mempool, error) { factory, found := coins.BlockChainFactories[coinName] if !found { return nil, nil, fmt.Errorf("Factory function not found") } chain, err := factory(cfg, func(_ bchain.NotificationType) {}) if err != nil { if isNetError(err) { return nil, nil, notConnectedError } return nil, nil, fmt.Errorf("Factory function failed: %s", err) } for i := 0; ; i++ { err = chain.Initialize() if err == nil { break } if isNetError(err) { return nil, nil, notConnectedError } // wait max 5 minutes for backend to startup if i > 5*60 { return nil, nil, fmt.Errorf("BlockChain initialization failed: %s", err) } time.Sleep(time.Millisecond * 1000) } if initMempool { mempool, err := chain.CreateMempool(chain) if err != nil { return nil, nil, fmt.Errorf("Mempool creation failed: %s", err) } err = chain.InitializeMempool(nil, nil, nil) if err != nil { return nil, nil, fmt.Errorf("Mempool initialization failed: %s", err) } return chain, mempool, nil } return chain, nil, nil } func isNetError(err error) bool { if _, ok := err.(net.Error); ok { return true } return false } func requiresMempool(cfg map[string]json.RawMessage) bool { tests, ok := cfg["rpc"] if !ok || len(tests) == 0 { return false } var rpcTests []string if err := json.Unmarshal(tests, &rpcTests); err != nil { return true } for _, test := range rpcTests { if test == "MempoolSync" || test == "GetTransactionForMempool" { return true } } return false } func hasConnectivity(cfg map[string]json.RawMessage) bool { _, ok := cfg["connectivity"] return ok }