449 lines
9.4 KiB
Go
449 lines
9.4 KiB
Go
// +build integration
|
|
|
|
package rpc
|
|
|
|
import (
|
|
"blockbook/bchain"
|
|
"encoding/json"
|
|
"math/rand"
|
|
"net"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/deckarep/golang-set"
|
|
)
|
|
|
|
type TestConfig struct {
|
|
URL string `json:"url"`
|
|
User string `json:"user"`
|
|
Pass string `json:"pass"`
|
|
}
|
|
|
|
type TestData struct {
|
|
BlockHeight uint32 `json:"blockHeight"`
|
|
BlockHash string `json:"blockHash"`
|
|
BlockTxs []string `json:"blockTxs"`
|
|
TxDetails map[string]*bchain.Tx `json:"txDetails"`
|
|
}
|
|
|
|
type Test struct {
|
|
Client bchain.BlockChain
|
|
TestData *TestData
|
|
connected bool
|
|
}
|
|
|
|
type TestChainFactoryFunc func(json.RawMessage) (bchain.BlockChain, error)
|
|
|
|
func NewTest(coin string, factory TestChainFactoryFunc) (*Test, error) {
|
|
var (
|
|
connected = true
|
|
cli bchain.BlockChain
|
|
cfg json.RawMessage
|
|
td *TestData
|
|
err error
|
|
)
|
|
|
|
cfg, err = LoadRPCConfig(coin)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cli, err = factory(cfg)
|
|
if err != nil {
|
|
if isNetError(err) {
|
|
connected = false
|
|
} else {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
td, err = LoadTestData(coin)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if td.TxDetails != nil {
|
|
parser := cli.GetChainParser()
|
|
|
|
for _, tx := range td.TxDetails {
|
|
err := setTxAddresses(parser, tx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
_, err = cli.GetBlockChainInfo()
|
|
if err != nil && isNetError(err) {
|
|
connected = false
|
|
}
|
|
}
|
|
|
|
return &Test{Client: cli, TestData: td, connected: connected}, nil
|
|
}
|
|
|
|
func isNetError(err error) bool {
|
|
if _, ok := err.(net.Error); ok {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func setTxAddresses(parser bchain.BlockChainParser, tx *bchain.Tx) error {
|
|
// pack and unpack transaction in order to get addresses decoded - ugly but works
|
|
var tmp *bchain.Tx
|
|
b, err := parser.PackTx(tx, 0, 0)
|
|
if err == nil {
|
|
tmp, _, err = parser.UnpackTx(b)
|
|
if err == nil {
|
|
for i := 0; i < len(tx.Vout); i++ {
|
|
tx.Vout[i].ScriptPubKey.Addresses = tmp.Vout[i].ScriptPubKey.Addresses
|
|
tx.Vout[i].Address = tmp.Vout[i].Address
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (rt *Test) skipUnconnected(t *testing.T) {
|
|
if !rt.connected {
|
|
t.Skip("Skipping test, not connected to backend service")
|
|
}
|
|
}
|
|
|
|
func (rt *Test) TestGetBlockHash(t *testing.T) {
|
|
rt.skipUnconnected(t)
|
|
|
|
hash, err := rt.Client.GetBlockHash(rt.TestData.BlockHeight)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
|
|
if hash != rt.TestData.BlockHash {
|
|
t.Errorf("GetBlockHash() got %q, want %q", hash, rt.TestData.BlockHash)
|
|
}
|
|
}
|
|
|
|
func (rt *Test) TestGetBlock(t *testing.T) {
|
|
rt.skipUnconnected(t)
|
|
|
|
blk, err := rt.Client.GetBlock(rt.TestData.BlockHash, 0)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
|
|
if len(blk.Txs) != len(rt.TestData.BlockTxs) {
|
|
t.Errorf("GetBlock() number of transactions: got %d, want %d", len(blk.Txs), len(rt.TestData.BlockTxs))
|
|
}
|
|
|
|
for ti, tx := range blk.Txs {
|
|
if tx.Txid != rt.TestData.BlockTxs[ti] {
|
|
t.Errorf("GetBlock() transaction %d: got %s, want %s", ti, tx.Txid, rt.TestData.BlockTxs[ti])
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func (rt *Test) TestGetTransaction(t *testing.T) {
|
|
rt.skipUnconnected(t)
|
|
|
|
for txid, want := range rt.TestData.TxDetails {
|
|
got, err := rt.Client.GetTransaction(txid)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
// Confirmations is variable field, we just check if is set and reset it
|
|
if got.Confirmations <= 0 {
|
|
t.Errorf("GetTransaction() got struct with invalid Confirmations field")
|
|
continue
|
|
}
|
|
got.Confirmations = 0
|
|
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("GetTransaction() got %v, want %v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rt *Test) TestGetTransactionForMempool(t *testing.T) {
|
|
rt.skipUnconnected(t)
|
|
|
|
for txid, want := range rt.TestData.TxDetails {
|
|
// reset fields that are not parsed by BlockChainParser
|
|
want.Confirmations, want.Blocktime, want.Time = 0, 0, 0
|
|
|
|
got, err := rt.Client.GetTransactionForMempool(txid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// transactions parsed from JSON may contain additional data
|
|
got.Confirmations, got.Blocktime, got.Time = 0, 0, 0
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("GetTransactionForMempool() got %v, want %v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rt *Test) getMempool(t *testing.T) []string {
|
|
txs, err := rt.Client.GetMempool()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(txs) == 0 {
|
|
t.Skip("Skipping test, mempool is empty")
|
|
}
|
|
|
|
return txs
|
|
}
|
|
|
|
func (rt *Test) getMempoolAddresses(t *testing.T, txs []string) map[string][]string {
|
|
txid2addrs := map[string][]string{}
|
|
for i := 0; i < len(txs); i++ {
|
|
tx, err := rt.Client.GetTransactionForMempool(txs[i])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
addrs := []string{}
|
|
for _, vin := range tx.Vin {
|
|
for _, a := range vin.Addresses {
|
|
addrs = append(addrs, a)
|
|
}
|
|
}
|
|
for _, vout := range tx.Vout {
|
|
for _, a := range vout.ScriptPubKey.Addresses {
|
|
addrs = append(addrs, a)
|
|
}
|
|
}
|
|
if len(addrs) > 0 {
|
|
txid2addrs[tx.Txid] = addrs
|
|
}
|
|
}
|
|
return txid2addrs
|
|
}
|
|
|
|
func (rt *Test) TestMempoolSync(t *testing.T) {
|
|
rt.skipUnconnected(t)
|
|
|
|
for i := 0; i < 3; i++ {
|
|
txs := rt.getMempool(t)
|
|
|
|
n, err := rt.Client.ResyncMempool(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n == 0 {
|
|
// no transactions to test
|
|
continue
|
|
}
|
|
|
|
txs = intersect(txs, rt.getMempool(t))
|
|
if len(txs) == 0 {
|
|
// no transactions to test
|
|
continue
|
|
}
|
|
|
|
txid2addrs := rt.getMempoolAddresses(t, txs)
|
|
if len(txid2addrs) == 0 {
|
|
t.Skip("Skipping test, no addresses in mempool")
|
|
}
|
|
|
|
for txid, addrs := range txid2addrs {
|
|
for _, a := range addrs {
|
|
got, err := rt.Client.GetMempoolTransactions(a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !containsString(got, txid) {
|
|
t.Errorf("ResyncMempool() - for address %s, transaction %s wasn't found in mempool", a, txid)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// done
|
|
return
|
|
}
|
|
t.Skip("Skipping test, all attempts to sync mempool failed due to network state changes")
|
|
}
|
|
|
|
func intersect(a, b []string) []string {
|
|
setA := mapset.NewSet()
|
|
for _, v := range a {
|
|
setA.Add(v)
|
|
}
|
|
setB := mapset.NewSet()
|
|
for _, v := range b {
|
|
setB.Add(v)
|
|
}
|
|
inter := setA.Intersect(setB)
|
|
res := make([]string, 0, inter.Cardinality())
|
|
for v := range inter.Iter() {
|
|
res = append(res, v.(string))
|
|
}
|
|
return res
|
|
}
|
|
|
|
func containsString(slice []string, s string) bool {
|
|
for i := 0; i < len(slice); i++ {
|
|
if slice[i] == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (rt *Test) TestGetMempoolEntry(t *testing.T) {
|
|
rt.skipUnconnected(t)
|
|
|
|
for i := 0; i < 3; i++ {
|
|
txs := rt.getMempool(t)
|
|
h, err := rt.Client.GetBestBlockHeight()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
txid := txs[rand.Intn(len(txs))]
|
|
tx, err := rt.Client.GetTransactionForMempool(txid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tx.Confirmations > 0 {
|
|
// tx confirmed
|
|
continue
|
|
}
|
|
|
|
e, err := rt.Client.GetMempoolEntry(txid)
|
|
if err != nil {
|
|
if err, ok := err.(*bchain.RPCError); ok && err.Code == -5 {
|
|
// tx confirmed
|
|
continue
|
|
}
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if d := int(e.Height) - int(h); d < -1 || d > 1 {
|
|
t.Errorf("GetMempoolEntry() got height %d, want %d", e.Height, h)
|
|
}
|
|
if e.Size <= 0 {
|
|
t.Errorf("GetMempoolEntry() got zero or negative size %d", e.Size)
|
|
}
|
|
if e.Fee <= 0 {
|
|
t.Errorf("GetMempoolEntry() got zero or negative fee %f", e.Fee)
|
|
}
|
|
|
|
// done
|
|
return
|
|
}
|
|
t.Skip("Skipping test, all attempts to get mempool entry failed due to network state changes")
|
|
}
|
|
|
|
func (rt *Test) TestEstimateSmartFee(t *testing.T) {
|
|
rt.skipUnconnected(t)
|
|
|
|
for _, blocks := range []int{1, 2, 3, 5, 10} {
|
|
fee, err := rt.Client.EstimateSmartFee(blocks, true)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if fee != -1 && fee < 0 {
|
|
t.Errorf("EstimateSmartFee() returned unexpected fee rate: %f", fee)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rt *Test) TestEstimateFee(t *testing.T) {
|
|
rt.skipUnconnected(t)
|
|
|
|
for _, blocks := range []int{1, 2, 3, 5, 10} {
|
|
fee, err := rt.Client.EstimateFee(blocks)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if fee != -1 && fee < 0 {
|
|
t.Errorf("EstimateFee() returned unexpected fee rate: %f", fee)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rt *Test) TestGetBestBlockHash(t *testing.T) {
|
|
rt.skipUnconnected(t)
|
|
|
|
for i := 0; i < 3; i++ {
|
|
hash, err := rt.Client.GetBestBlockHash()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
height, err := rt.Client.GetBestBlockHeight()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hh, err := rt.Client.GetBlockHash(height)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if hash != hh {
|
|
continue
|
|
}
|
|
|
|
// we expect no next block
|
|
_, err = rt.Client.GetBlock("", height+1)
|
|
if err != nil {
|
|
if err != bchain.ErrBlockNotFound {
|
|
t.Error(err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Error("GetBestBlockHash() didn't get the best hash")
|
|
}
|
|
|
|
func (rt *Test) TestGetBestBlockHeight(t *testing.T) {
|
|
rt.skipUnconnected(t)
|
|
|
|
for i := 0; i < 3; i++ {
|
|
height, err := rt.Client.GetBestBlockHeight()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// we expect no next block
|
|
_, err = rt.Client.GetBlock("", height+1)
|
|
if err != nil {
|
|
if err != bchain.ErrBlockNotFound {
|
|
t.Error(err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Error("GetBestBlockHeigh() didn't get the the best heigh")
|
|
}
|
|
|
|
func (rt *Test) TestGetBlockHeader(t *testing.T) {
|
|
rt.skipUnconnected(t)
|
|
|
|
want := &bchain.BlockHeader{
|
|
Hash: rt.TestData.BlockHash,
|
|
Height: rt.TestData.BlockHeight,
|
|
}
|
|
|
|
got, err := rt.Client.GetBlockHeader(rt.TestData.BlockHash)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Confirmations is variable field, we just check if is set and reset it
|
|
if got.Confirmations <= 0 {
|
|
t.Fatalf("GetBlockHeader() got struct with invalid Confirmations field")
|
|
}
|
|
got.Confirmations = 0
|
|
|
|
got.Prev, got.Next = "", ""
|
|
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("GetBlockHeader() got=%v, want=%v", got, want)
|
|
}
|
|
}
|