2023-11-04 23:59:30 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/hex"
|
|
|
|
"errors"
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
"github.com/btcsuite/btcd/btcjson"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
|
|
"github.com/btcsuite/btcd/mempool"
|
2024-02-23 08:29:57 +01:00
|
|
|
"github.com/btcsuite/btcd/wire"
|
2023-11-04 23:59:30 +01:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
|
|
|
|
|
|
|
// TestHandleTestMempoolAcceptFailDecode checks that when invalid hex string is
|
|
|
|
// used as the raw txns, the corresponding error is returned.
|
|
|
|
func TestHandleTestMempoolAcceptFailDecode(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
// Create a testing server.
|
|
|
|
s := &rpcServer{}
|
|
|
|
|
|
|
|
testCases := []struct {
|
|
|
|
name string
|
|
|
|
txns []string
|
|
|
|
expectedErrCode btcjson.RPCErrorCode
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "hex decode fail",
|
|
|
|
txns: []string{"invalid"},
|
|
|
|
expectedErrCode: btcjson.ErrRPCDecodeHexString,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "tx decode fail",
|
|
|
|
txns: []string{"696e76616c6964"},
|
|
|
|
expectedErrCode: btcjson.ErrRPCDeserialization,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tc := range testCases {
|
|
|
|
tc := tc
|
|
|
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
// Create a request that uses invalid raw txns.
|
|
|
|
cmd := btcjson.NewTestMempoolAcceptCmd(tc.txns, 0)
|
|
|
|
|
|
|
|
// Call the method under test.
|
|
|
|
closeChan := make(chan struct{})
|
|
|
|
result, err := handleTestMempoolAccept(
|
|
|
|
s, cmd, closeChan,
|
|
|
|
)
|
|
|
|
|
|
|
|
// Ensure the expected error is returned.
|
|
|
|
require.Error(err)
|
|
|
|
rpcErr, ok := err.(*btcjson.RPCError)
|
|
|
|
require.True(ok)
|
|
|
|
require.Equal(tc.expectedErrCode, rpcErr.Code)
|
|
|
|
|
|
|
|
// No result should be returned.
|
|
|
|
require.Nil(result)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
// TODO(yy): make a `btctest` package and move these testing txns there
|
|
|
|
// so they be used in other tests.
|
|
|
|
//
|
|
|
|
// txHex1 is taken from `txscript/data/tx_valid.json`.
|
|
|
|
txHex1 = "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b" +
|
|
|
|
"49aa43ad90ba26000000000490047304402203f16c6f40162ab686621ef3" +
|
|
|
|
"000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507a" +
|
|
|
|
"c48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0" +
|
|
|
|
"140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271a" +
|
|
|
|
"d504b88ac00000000"
|
|
|
|
|
|
|
|
// txHex2 is taken from `txscript/data/tx_valid.json`.
|
|
|
|
txHex2 = "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b" +
|
|
|
|
"49aa43ad90ba260000000004a0048304402203f16c6f40162ab686621ef3" +
|
|
|
|
"000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507a" +
|
|
|
|
"c48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2bab01fffffff" +
|
|
|
|
"f0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c27" +
|
|
|
|
"1ad504b88ac00000000"
|
|
|
|
|
|
|
|
// txHex3 is taken from `txscript/data/tx_valid.json`.
|
|
|
|
txHex3 = "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b" +
|
|
|
|
"49aa43ad90ba260000000004a01ff47304402203f16c6f40162ab686621e" +
|
|
|
|
"f3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc350" +
|
|
|
|
"7ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01fffffff" +
|
|
|
|
"f0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c27" +
|
|
|
|
"1ad504b88ac00000000"
|
|
|
|
)
|
|
|
|
|
|
|
|
// decodeTxHex decodes the given hex string into a transaction.
|
|
|
|
func decodeTxHex(t *testing.T, txHex string) *btcutil.Tx {
|
|
|
|
rawBytes, err := hex.DecodeString(txHex)
|
|
|
|
require.NoError(t, err)
|
|
|
|
tx, err := btcutil.NewTxFromBytes(rawBytes)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
return tx
|
|
|
|
}
|
|
|
|
|
|
|
|
// TestHandleTestMempoolAcceptMixedResults checks that when different txns get
|
|
|
|
// different responses from calling the mempool method `CheckMempoolAcceptance`
|
|
|
|
// their results are correctly returned.
|
|
|
|
func TestHandleTestMempoolAcceptMixedResults(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
// Create a mock mempool.
|
|
|
|
mm := &mempool.MockTxMempool{}
|
|
|
|
|
|
|
|
// Create a testing server with the mock mempool.
|
|
|
|
s := &rpcServer{cfg: rpcserverConfig{
|
|
|
|
TxMemPool: mm,
|
|
|
|
}}
|
|
|
|
|
|
|
|
// Decode the hex so we can assert the mock mempool is called with it.
|
|
|
|
tx1 := decodeTxHex(t, txHex1)
|
|
|
|
tx2 := decodeTxHex(t, txHex2)
|
|
|
|
tx3 := decodeTxHex(t, txHex3)
|
|
|
|
|
|
|
|
// Create a slice to hold the expected results. We will use three txns
|
|
|
|
// so we expect threeresults.
|
|
|
|
expectedResults := make([]*btcjson.TestMempoolAcceptResult, 3)
|
|
|
|
|
|
|
|
// We now mock the first call to `CheckMempoolAcceptance` to return an
|
|
|
|
// error.
|
|
|
|
dummyErr := errors.New("dummy error")
|
|
|
|
mm.On("CheckMempoolAcceptance", tx1).Return(nil, dummyErr).Once()
|
|
|
|
|
|
|
|
// Since the call failed, we expect the first result to give us the
|
|
|
|
// error.
|
|
|
|
expectedResults[0] = &btcjson.TestMempoolAcceptResult{
|
|
|
|
Txid: tx1.Hash().String(),
|
|
|
|
Wtxid: tx1.WitnessHash().String(),
|
|
|
|
Allowed: false,
|
|
|
|
RejectReason: dummyErr.Error(),
|
|
|
|
}
|
|
|
|
|
|
|
|
// We mock the second call to `CheckMempoolAcceptance` to return a
|
|
|
|
// result saying the tx is missing inputs.
|
|
|
|
mm.On("CheckMempoolAcceptance", tx2).Return(
|
|
|
|
&mempool.MempoolAcceptResult{
|
|
|
|
MissingParents: []*chainhash.Hash{},
|
|
|
|
}, nil,
|
|
|
|
).Once()
|
|
|
|
|
|
|
|
// We expect the second result to give us the missing-inputs error.
|
|
|
|
expectedResults[1] = &btcjson.TestMempoolAcceptResult{
|
|
|
|
Txid: tx2.Hash().String(),
|
|
|
|
Wtxid: tx2.WitnessHash().String(),
|
|
|
|
Allowed: false,
|
|
|
|
RejectReason: "missing-inputs",
|
|
|
|
}
|
|
|
|
|
|
|
|
// We mock the third call to `CheckMempoolAcceptance` to return a
|
|
|
|
// result saying the tx allowed.
|
|
|
|
const feeSats = btcutil.Amount(1000)
|
|
|
|
mm.On("CheckMempoolAcceptance", tx3).Return(
|
|
|
|
&mempool.MempoolAcceptResult{
|
|
|
|
TxFee: feeSats,
|
|
|
|
TxSize: 100,
|
|
|
|
}, nil,
|
|
|
|
).Once()
|
|
|
|
|
|
|
|
// We expect the third result to give us the fee details.
|
|
|
|
expectedResults[2] = &btcjson.TestMempoolAcceptResult{
|
|
|
|
Txid: tx3.Hash().String(),
|
|
|
|
Wtxid: tx3.WitnessHash().String(),
|
|
|
|
Allowed: true,
|
|
|
|
Vsize: 100,
|
|
|
|
Fees: &btcjson.TestMempoolAcceptFees{
|
|
|
|
Base: feeSats.ToBTC(),
|
|
|
|
EffectiveFeeRate: feeSats.ToBTC() * 1e3 / 100,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a mock request with default max fee rate of 0.1 BTC/KvB.
|
|
|
|
cmd := btcjson.NewTestMempoolAcceptCmd(
|
|
|
|
[]string{txHex1, txHex2, txHex3}, 0.1,
|
|
|
|
)
|
|
|
|
|
|
|
|
// Call the method handler and assert the expected results are
|
|
|
|
// returned.
|
|
|
|
closeChan := make(chan struct{})
|
|
|
|
results, err := handleTestMempoolAccept(s, cmd, closeChan)
|
|
|
|
require.NoError(err)
|
|
|
|
require.Equal(expectedResults, results)
|
|
|
|
|
|
|
|
// Assert the mocked method is called as expected.
|
|
|
|
mm.AssertExpectations(t)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TestValidateFeeRate checks that `validateFeeRate` behaves as expected.
|
|
|
|
func TestValidateFeeRate(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
const (
|
|
|
|
// testFeeRate is in BTC/kvB.
|
|
|
|
testFeeRate = 0.1
|
|
|
|
|
|
|
|
// testTxSize is in vb.
|
|
|
|
testTxSize = 100
|
|
|
|
|
|
|
|
// testFeeSats is in sats.
|
|
|
|
// We have 0.1BTC/kvB =
|
|
|
|
// 0.1 * 1e8 sats/kvB =
|
|
|
|
// 0.1 * 1e8 / 1e3 sats/vb = 0.1 * 1e5 sats/vb.
|
|
|
|
testFeeSats = btcutil.Amount(testFeeRate * 1e5 * testTxSize)
|
|
|
|
)
|
|
|
|
|
|
|
|
testCases := []struct {
|
|
|
|
name string
|
|
|
|
feeSats btcutil.Amount
|
|
|
|
txSize int64
|
|
|
|
maxFeeRate float64
|
|
|
|
expectedFees *btcjson.TestMempoolAcceptFees
|
|
|
|
allowed bool
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
// When the fee rate(0.1) is above the max fee
|
|
|
|
// rate(0.01), we expect a nil result and false.
|
|
|
|
name: "fee rate above max",
|
|
|
|
feeSats: testFeeSats,
|
|
|
|
txSize: testTxSize,
|
|
|
|
maxFeeRate: testFeeRate / 10,
|
|
|
|
expectedFees: nil,
|
|
|
|
allowed: false,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
// When the fee rate(0.1) is no greater than the max
|
|
|
|
// fee rate(0.1), we expect a result and true.
|
|
|
|
name: "fee rate below max",
|
|
|
|
feeSats: testFeeSats,
|
|
|
|
txSize: testTxSize,
|
|
|
|
maxFeeRate: testFeeRate,
|
|
|
|
expectedFees: &btcjson.TestMempoolAcceptFees{
|
|
|
|
Base: testFeeSats.ToBTC(),
|
|
|
|
EffectiveFeeRate: testFeeRate,
|
|
|
|
},
|
|
|
|
allowed: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
// When the fee rate(1) is above the default max fee
|
|
|
|
// rate(0.1), we expect a nil result and false.
|
|
|
|
name: "fee rate above default max",
|
|
|
|
feeSats: testFeeSats,
|
|
|
|
txSize: testTxSize / 10,
|
|
|
|
expectedFees: nil,
|
|
|
|
allowed: false,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
// When the fee rate(0.1) is no greater than the
|
|
|
|
// default max fee rate(0.1), we expect a result and
|
|
|
|
// true.
|
|
|
|
name: "fee rate below default max",
|
|
|
|
feeSats: testFeeSats,
|
|
|
|
txSize: testTxSize,
|
|
|
|
expectedFees: &btcjson.TestMempoolAcceptFees{
|
|
|
|
Base: testFeeSats.ToBTC(),
|
|
|
|
EffectiveFeeRate: testFeeRate,
|
|
|
|
},
|
|
|
|
allowed: true,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tc := range testCases {
|
|
|
|
tc := tc
|
|
|
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
result, allowed := validateFeeRate(
|
|
|
|
tc.feeSats, tc.txSize, tc.maxFeeRate,
|
|
|
|
)
|
|
|
|
|
|
|
|
require.Equal(tc.expectedFees, result)
|
|
|
|
require.Equal(tc.allowed, allowed)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TestHandleTestMempoolAcceptFees checks that the `Fees` field is correctly
|
|
|
|
// populated based on the max fee rate and the tx being checked.
|
|
|
|
func TestHandleTestMempoolAcceptFees(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
// Create a mock mempool.
|
|
|
|
mm := &mempool.MockTxMempool{}
|
|
|
|
|
|
|
|
// Create a testing server with the mock mempool.
|
|
|
|
s := &rpcServer{cfg: rpcserverConfig{
|
|
|
|
TxMemPool: mm,
|
|
|
|
}}
|
|
|
|
|
|
|
|
const (
|
|
|
|
// Set transaction's fee rate to be 0.2BTC/kvB.
|
|
|
|
feeRate = defaultMaxFeeRate * 2
|
|
|
|
|
|
|
|
// txSize is 100vb.
|
|
|
|
txSize = 100
|
|
|
|
|
|
|
|
// feeSats is 2e6 sats.
|
|
|
|
feeSats = feeRate * 1e8 * txSize / 1e3
|
|
|
|
)
|
|
|
|
|
|
|
|
testCases := []struct {
|
|
|
|
name string
|
|
|
|
maxFeeRate float64
|
|
|
|
txHex string
|
|
|
|
rejectReason string
|
|
|
|
allowed bool
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
// When the fee rate(0.2) used by the tx is below the
|
|
|
|
// max fee rate(2) specified, the result should allow
|
|
|
|
// it.
|
|
|
|
name: "below max fee rate",
|
|
|
|
maxFeeRate: feeRate * 10,
|
|
|
|
txHex: txHex1,
|
|
|
|
allowed: true,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
// When the fee rate(0.2) used by the tx is above the
|
|
|
|
// max fee rate(0.02) specified, the result should
|
|
|
|
// disallow it.
|
|
|
|
name: "above max fee rate",
|
|
|
|
maxFeeRate: feeRate / 10,
|
|
|
|
txHex: txHex1,
|
|
|
|
allowed: false,
|
|
|
|
rejectReason: "max-fee-exceeded",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
// When the max fee rate is not set, the default
|
|
|
|
// 0.1BTC/kvB is used and the fee rate(0.2) used by the
|
|
|
|
// tx is above it, the result should disallow it.
|
|
|
|
name: "above default max fee rate",
|
|
|
|
txHex: txHex1,
|
|
|
|
allowed: false,
|
|
|
|
rejectReason: "max-fee-exceeded",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tc := range testCases {
|
|
|
|
tc := tc
|
|
|
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
// Decode the hex so we can assert the mock mempool is
|
|
|
|
// called with it.
|
|
|
|
tx := decodeTxHex(t, txHex1)
|
|
|
|
|
|
|
|
// We mock the call to `CheckMempoolAcceptance` to
|
|
|
|
// return the result.
|
|
|
|
mm.On("CheckMempoolAcceptance", tx).Return(
|
|
|
|
&mempool.MempoolAcceptResult{
|
|
|
|
TxFee: feeSats,
|
|
|
|
TxSize: txSize,
|
|
|
|
}, nil,
|
|
|
|
).Once()
|
|
|
|
|
|
|
|
// We expect the third result to give us the fee
|
|
|
|
// details.
|
|
|
|
expected := &btcjson.TestMempoolAcceptResult{
|
|
|
|
Txid: tx.Hash().String(),
|
|
|
|
Wtxid: tx.WitnessHash().String(),
|
|
|
|
Allowed: tc.allowed,
|
|
|
|
}
|
|
|
|
|
|
|
|
if tc.allowed {
|
|
|
|
expected.Vsize = txSize
|
|
|
|
expected.Fees = &btcjson.TestMempoolAcceptFees{
|
|
|
|
Base: feeSats / 1e8,
|
|
|
|
EffectiveFeeRate: feeRate,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
expected.RejectReason = tc.rejectReason
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a mock request with specified max fee rate.
|
|
|
|
cmd := btcjson.NewTestMempoolAcceptCmd(
|
|
|
|
[]string{txHex1}, tc.maxFeeRate,
|
|
|
|
)
|
|
|
|
|
|
|
|
// Call the method handler and assert the expected
|
|
|
|
// result is returned.
|
|
|
|
closeChan := make(chan struct{})
|
|
|
|
r, err := handleTestMempoolAccept(s, cmd, closeChan)
|
|
|
|
require.NoError(err)
|
|
|
|
|
|
|
|
// Check the interface type.
|
|
|
|
results, ok := r.([]*btcjson.TestMempoolAcceptResult)
|
|
|
|
require.True(ok)
|
|
|
|
|
|
|
|
// Expect exactly one result.
|
|
|
|
require.Len(results, 1)
|
|
|
|
|
|
|
|
// Check the result is returned as expected.
|
|
|
|
require.Equal(expected, results[0])
|
|
|
|
|
|
|
|
// Assert the mocked method is called as expected.
|
|
|
|
mm.AssertExpectations(t)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2024-02-23 08:29:57 +01:00
|
|
|
|
|
|
|
// TestGetTxSpendingPrevOut checks that handleGetTxSpendingPrevOut handles the
|
|
|
|
// cmd as expected.
|
|
|
|
func TestGetTxSpendingPrevOut(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
// Create a mock mempool.
|
|
|
|
mm := &mempool.MockTxMempool{}
|
|
|
|
defer mm.AssertExpectations(t)
|
|
|
|
|
|
|
|
// Create a testing server with the mock mempool.
|
|
|
|
s := &rpcServer{cfg: rpcserverConfig{
|
|
|
|
TxMemPool: mm,
|
|
|
|
}}
|
|
|
|
|
|
|
|
// First, check the error case.
|
|
|
|
//
|
|
|
|
// Create a request that will cause an error.
|
|
|
|
cmd := &btcjson.GetTxSpendingPrevOutCmd{
|
|
|
|
Outputs: []*btcjson.GetTxSpendingPrevOutCmdOutput{
|
|
|
|
{Txid: "invalid"},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// Call the method handler and assert the error is returned.
|
|
|
|
closeChan := make(chan struct{})
|
|
|
|
results, err := handleGetTxSpendingPrevOut(s, cmd, closeChan)
|
|
|
|
require.Error(err)
|
|
|
|
require.Nil(results)
|
|
|
|
|
|
|
|
// We now check the normal case. Two outputs will be tested - one found
|
|
|
|
// in mempool and other not.
|
|
|
|
//
|
|
|
|
// Decode the hex so we can assert the mock mempool is called with it.
|
|
|
|
tx := decodeTxHex(t, txHex1)
|
|
|
|
|
|
|
|
// Create testing outpoints.
|
|
|
|
opInMempool := wire.OutPoint{Hash: chainhash.Hash{1}, Index: 1}
|
|
|
|
opNotInMempool := wire.OutPoint{Hash: chainhash.Hash{2}, Index: 1}
|
|
|
|
|
|
|
|
// We only expect to see one output being found as spent in mempool.
|
|
|
|
expectedResults := []*btcjson.GetTxSpendingPrevOutResult{
|
|
|
|
{
|
|
|
|
Txid: opInMempool.Hash.String(),
|
|
|
|
Vout: opInMempool.Index,
|
|
|
|
SpendingTxid: tx.Hash().String(),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Txid: opNotInMempool.Hash.String(),
|
|
|
|
Vout: opNotInMempool.Index,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// We mock the first call to `CheckSpend` to return a result saying the
|
|
|
|
// output is found.
|
|
|
|
mm.On("CheckSpend", opInMempool).Return(tx).Once()
|
|
|
|
|
|
|
|
// We mock the second call to `CheckSpend` to return a result saying the
|
|
|
|
// output is NOT found.
|
|
|
|
mm.On("CheckSpend", opNotInMempool).Return(nil).Once()
|
|
|
|
|
|
|
|
// Create a request with the above outputs.
|
|
|
|
cmd = &btcjson.GetTxSpendingPrevOutCmd{
|
|
|
|
Outputs: []*btcjson.GetTxSpendingPrevOutCmdOutput{
|
|
|
|
{
|
|
|
|
Txid: opInMempool.Hash.String(),
|
|
|
|
Vout: opInMempool.Index,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Txid: opNotInMempool.Hash.String(),
|
|
|
|
Vout: opNotInMempool.Index,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// Call the method handler and assert the expected result is returned.
|
|
|
|
closeChan = make(chan struct{})
|
|
|
|
results, err = handleGetTxSpendingPrevOut(s, cmd, closeChan)
|
|
|
|
require.NoError(err)
|
|
|
|
require.Equal(expectedResults, results)
|
|
|
|
}
|