//go:build integration package api import ( "crypto/tls" "encoding/json" "errors" "net/http" "testing" "time" "github.com/trezor/blockbook/bchain" ) const ( httpTimeout = 30 * time.Second wsDialTimeout = 10 * time.Second wsMessageTimeout = 15 * time.Second txSearchWindow = 12 blockPageSize = 1 sampleBlockPageSize = 3 ) type testCapability uint8 const ( capabilityNone testCapability = 0 capabilityUTXO testCapability = 1 << iota capabilityEVM ) type testDefinition struct { fn func(t *testing.T, th *TestHandler) required testCapability group string } var commonTests = map[string]func(t *testing.T, th *TestHandler){ "Status": testStatus, "GetBlockIndex": testGetBlockIndex, "GetBlockByHeight": testGetBlockByHeight, "GetBlock": testGetBlock, "GetTransaction": testGetTransaction, "GetTransactionSpecific": testGetTransactionSpecific, "GetAddress": testGetAddress, "GetAddressTxids": testGetAddressTxids, "GetAddressTxs": testGetAddressTxs, } var utxoOnlyTests = map[string]func(t *testing.T, th *TestHandler){ "GetUtxo": testGetUtxo, "GetUtxoConfirmedFilter": testGetUtxoConfirmedFilter, } var evmOnlyTests = map[string]func(t *testing.T, th *TestHandler){ "GetAddressBasicEVM": testGetAddressBasicEVM, "GetAddressTokensEVM": testGetAddressTokensEVM, "GetAddressTokenBalances": testGetAddressTokenBalances, "GetAddressTxidsPaginationEVM": testGetAddressTxidsPaginationEVM, "GetAddressTxsPaginationEVM": testGetAddressTxsPaginationEVM, "GetAddressContractFilterEVM": testGetAddressContractFilterEVM, "GetTransactionEVMShape": testGetTransactionEVMShape, "WsGetAccountInfoBasicEVM": testWsGetAccountInfoBasicEVM, "WsGetAccountInfoEVM": testWsGetAccountInfoEVM, "WsGetAccountInfoTxidsConsistencyEVM": testWsGetAccountInfoTxidsConsistencyEVM, "WsGetAccountInfoTxsConsistencyEVM": testWsGetAccountInfoTxsConsistencyEVM, "WsGetAccountInfoContractFilterEVM": testWsGetAccountInfoContractFilterEVM, } var wsOnlyTests = map[string]func(t *testing.T, th *TestHandler){ "WsGetInfo": testWsGetInfo, "WsGetBlockHash": testWsGetBlockHash, "WsGetTransaction": testWsGetTransaction, "WsGetAccountInfo": testWsGetAccountInfo, "WsPing": testWsPing, } var wsUTXOTests = map[string]func(t *testing.T, th *TestHandler){ "WsGetAccountUtxo": testWsGetAccountUtxo, } var testRegistry = buildTestRegistry() type TestHandler struct { Coin string HTTPBase string WSURL string HTTP *http.Client status *statusBlockbook nextWSReq int blockHashByHeight map[int]string blockByHash map[string]*blockSummary txByID map[string]*txDetailResponse sampleTxResolved bool sampleTxID string sampleAddrResolved bool sampleAddress string sampleIndexResolved bool sampleIndexHeight int sampleIndexHash string sampleBlockResolved bool sampleBlockHeight int sampleBlockHash string sampleContractResolved bool sampleContract string capabilitiesResolved bool supportsUTXO bool utxoProbeMessage string supportsEVM bool evmProbeMessage string } type statusEnvelope struct { Blockbook json.RawMessage `json:"blockbook"` Backend json.RawMessage `json:"backend"` } type statusBlockbook struct { BestHeight int `json:"bestHeight"` } type blockIndexResponse struct { BlockHash string `json:"blockHash"` } type blockResponse struct { Hash string `json:"hash"` Height int `json:"height"` Txs []json.RawMessage `json:"txs"` } type blockSummary struct { Hash string Height int HasTxField bool TxIDs []string } type txPart struct { Addresses []string `json:"addresses"` } type txDetailResponse struct { Txid string `json:"txid"` Vin []txPart `json:"vin"` Vout []txPart `json:"vout"` } type addressResponse struct { Address string `json:"address"` } type addressTxidsResponse struct { Address string `json:"address"` Page int `json:"page"` ItemsOnPage int `json:"itemsOnPage"` TotalPages int `json:"totalPages"` Txs int `json:"txs"` Txids []string `json:"txids"` } type addressTxsResponse struct { Address string `json:"address"` Page int `json:"page"` ItemsOnPage int `json:"itemsOnPage"` TotalPages int `json:"totalPages"` Txs int `json:"txs"` Transactions []txDetailResponse `json:"transactions"` } type utxoResponse struct { Txid string `json:"txid"` Vout int `json:"vout"` Value string `json:"value"` Confirmations int `json:"confirmations"` Height int `json:"height"` } type wsRequest struct { ID string `json:"id"` Method string `json:"method"` Params interface{} `json:"params"` } type wsResponse struct { ID string `json:"id"` Data json.RawMessage `json:"data"` } type wsInfoResponse struct { BestHeight int `json:"bestHeight"` BestHash string `json:"bestHash"` } type wsBlockHashResponse struct { Hash string `json:"hash"` } type evmAddressTokenBalanceResponse struct { Address string `json:"address"` Balance string `json:"balance"` Nonce string `json:"nonce"` Txs int `json:"txs"` NonTokenTxs int `json:"nonTokenTxs"` Tokens []evmTokenResponse `json:"tokens"` } type evmTokenResponse struct { Type string `json:"type"` Standard string `json:"standard"` Contract string `json:"contract"` Balance string `json:"balance"` IDs []string `json:"ids"` MultiTokenValues []evmMultiTokenValue `json:"multiTokenValues"` } type evmMultiTokenValue struct { ID string `json:"id"` Value string `json:"value"` } type evmTxShapeResponse struct { Txid string `json:"txid"` Vin []txPart `json:"vin"` Vout []txPart `json:"vout"` EthereumSpecific json.RawMessage `json:"ethereumSpecific"` } type coinConfig struct { Coin struct { Alias string `json:"alias"` } `json:"coin"` Ports struct { BlockbookPublic int `json:"blockbook_public"` } `json:"ports"` } type apiEndpoints struct { HTTP string WS string } func IntegrationTest(t *testing.T, coin string, _ bchain.BlockChain, _ bchain.Mempool, testConfig json.RawMessage) { tests, err := getTests(testConfig) if err != nil { t.Fatalf("failed loading api test list: %v", err) } endpoints, err := resolveAPIEndpoints(coin) if err != nil { t.Fatalf("resolve API endpoints for %s: %v", coin, err) } h := &TestHandler{ Coin: coin, HTTPBase: endpoints.HTTP, WSURL: endpoints.WS, HTTP: newHTTPClient(), blockHashByHeight: make(map[int]string), blockByHash: make(map[string]*blockSummary), txByID: make(map[string]*txDetailResponse), } for _, test := range tests { if td, found := testRegistry[test]; found { t.Run(test, func(t *testing.T) { if !h.requireCapabilities(t, td.required, td.group, test) { return } td.fn(t, h) }) } else { t.Errorf("%s: test not found", test) } } } func buildTestRegistry() map[string]testDefinition { registry := make(map[string]testDefinition, len(commonTests)+len(utxoOnlyTests)+len(evmOnlyTests)+len(wsOnlyTests)+len(wsUTXOTests)) addGroup := func(group string, required testCapability, tests map[string]func(t *testing.T, th *TestHandler)) { for name, fn := range tests { if _, found := registry[name]; found { panic("duplicate api test definition: " + name) } registry[name] = testDefinition{ fn: fn, required: required, group: group, } } } addGroup("common", capabilityNone, commonTests) addGroup("utxo-only", capabilityUTXO, utxoOnlyTests) addGroup("evm-only", capabilityEVM, evmOnlyTests) addGroup("ws-only", capabilityNone, wsOnlyTests) addGroup("ws-utxo", capabilityUTXO, wsUTXOTests) return registry } func getTests(cfg json.RawMessage) ([]string, error) { var v []string if err := json.Unmarshal(cfg, &v); err != nil { return nil, err } if len(v) == 0 { return nil, errors.New("no tests declared") } return v, nil } func newHTTPClient() *http.Client { return &http.Client{ Timeout: httpTimeout, Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, }, } }