Add tests for bitcoin client get count method (#1530)

* Add tests for bitcoin client get count method

* Rename bitcoin exceptions

* Got test working for bitcoin http error

* Added tests for get block count and get block hash

* Added tests for all bitcoin client methods

* Added tests for invalid results

* Remove check for empty result which is not needed

* Add test for all bitcoin request errors

* Rename bitcoin connection error exception

* Remove print statments from bitcoin client

* Got 100% coverage for bitcoin client

* Add todos to assert mocks called with correct args

* Rename bitcoin core client class and module
This commit is contained in:
Jonathan Zernik 2021-10-07 19:42:15 -07:00 committed by GitHub
parent 7a88fd95ed
commit e0c63e2927
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 308 additions and 31 deletions

View file

@ -27,11 +27,12 @@ import requests
from squeaknode.bitcoin.bitcoin_client import BitcoinClient
from squeaknode.bitcoin.block_info import BlockInfo
from squeaknode.bitcoin.exception import BitcoinRequestError
logger = logging.getLogger(__name__)
class BitcoinCoreBitcoinClient(BitcoinClient):
class BitcoinCoreClient(BitcoinClient):
"""Access a bitcoin daemon using RPC."""
def __init__(
@ -59,7 +60,7 @@ class BitcoinCoreBitcoinClient(BitcoinClient):
def get_block_info_by_height(self, block_height: int) -> BlockInfo:
block_hash = self.get_block_hash(block_height)
block_header = self.get_block_header(block_hash, False)
block_header = self.get_block_header(block_hash)
return BlockInfo(block_height, block_hash, block_header)
def get_block_count(self) -> int:
@ -69,16 +70,8 @@ class BitcoinCoreBitcoinClient(BitcoinClient):
"jsonrpc": "2.0",
"id": 0,
}
response = requests.post(
self.url,
data=json.dumps(payload),
headers=self.headers,
).json()
logger.debug("Got response for get_block_count: {}".format(response))
result = response["result"]
if result is None:
raise Exception("Unable to get block count from bitcoin node.")
json_response = self.make_request(payload)
result = json_response["result"]
block_count = int(result)
logger.debug("Got block_count: {}".format(block_count))
return block_count
@ -90,32 +83,39 @@ class BitcoinCoreBitcoinClient(BitcoinClient):
"jsonrpc": "2.0",
"id": 0,
}
response = requests.post(
self.url,
data=json.dumps(payload),
headers=self.headers,
).json()
logger.debug("Got response for get_block_hash: {}".format(response))
result = response["result"]
json_response = self.make_request(payload)
result = json_response["result"]
block_hash = result
logger.debug("Got block_hash: {}".format(block_hash))
return bytes.fromhex(block_hash)
def get_block_header(self, block_hash: bytes, verbose: bool) -> bytes:
def get_block_header(self, block_hash: bytes, verbose: bool = False) -> bytes:
payload = {
"method": "getblockheader",
"params": [block_hash.hex(), verbose],
"jsonrpc": "2.0",
"id": 0,
}
response = requests.post(
self.url,
data=json.dumps(payload),
headers=self.headers,
).json()
logger.debug("Got response for get_block_header: {}".format(response))
result = response["result"]
json_response = self.make_request(payload)
result = json_response["result"]
logger.debug("Got block_header: {}".format(result))
return bytes.fromhex(result)
def make_request(self, payload: dict) -> dict:
try:
response = requests.post(
self.url,
data=json.dumps(payload),
headers=self.headers,
)
response.raise_for_status()
except requests.exceptions.HTTPError as errh:
raise BitcoinRequestError(errh)
except requests.exceptions.ConnectionError as errc:
raise BitcoinRequestError(errc)
except requests.exceptions.Timeout as errt:
raise BitcoinRequestError(errt)
except requests.exceptions.RequestException as err:
raise BitcoinRequestError(err)
return response.json()

View file

@ -0,0 +1,37 @@
# MIT License
#
# Copyright (c) 2020 Jonathan Zernik
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
class BitcoinError(Exception):
"""Base class for other bitcoin exceptions."""
class BitcoinRequestError(BitcoinError):
"""Error that is raised when the bitcoin connection fails."""
def __init__(self, err):
self.err = err
def __repr__(self):
return 'BitcoinRequestError(%r)' % (
self.err,
)

View file

@ -28,7 +28,7 @@ from squeaknode.admin.squeak_admin_server_handler import SqueakAdminServerHandle
from squeaknode.admin.squeak_admin_server_servicer import SqueakAdminServerServicer
from squeaknode.admin.webapp.app import SqueakAdminWebServer
from squeaknode.bitcoin.bitcoin_block_subscription_client import BitcoinBlockSubscriptionClient
from squeaknode.bitcoin.bitcoin_core_bitcoin_client import BitcoinCoreBitcoinClient
from squeaknode.bitcoin.bitcoin_core_client import BitcoinCoreClient
from squeaknode.config.config import SqueaknodeConfig
from squeaknode.core.squeak_core import SqueakCore
from squeaknode.db.db_engine import get_connection_string
@ -131,7 +131,7 @@ class SqueakNode:
def initialize_bitcoin_client(self):
# load the bitcoin client
self.bitcoin_client = BitcoinCoreBitcoinClient(
self.bitcoin_client = BitcoinCoreClient(
self.config.bitcoin.rpc_host,
self.config.bitcoin.rpc_port,
self.config.bitcoin.rpc_user,

View file

@ -0,0 +1,240 @@
# MIT License
#
# Copyright (c) 2020 Jonathan Zernik
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import mock
import pytest
from requests import HTTPError
from requests.exceptions import ConnectionError
from requests.exceptions import RequestException
from requests.exceptions import Timeout
from squeaknode.bitcoin.bitcoin_core_client import BitcoinCoreClient
from squeaknode.bitcoin.block_info import BlockInfo
from squeaknode.bitcoin.exception import BitcoinRequestError
@pytest.fixture
def bitcoin_core_client():
yield BitcoinCoreClient(
host="fake_bitcoin_host",
port=5678,
rpc_user="fake_user",
rpc_password="fake_pass",
use_ssl=False,
ssl_cert="fake_ssl_cert",
)
@pytest.fixture
def block_count():
yield 555
@pytest.fixture
def block_hash_str():
yield '00000000edade40797e3c4bf27edeb65733d1884beaa8c502a89d50a54111e1c'
@pytest.fixture
def block_hash(block_hash_str):
yield bytes.fromhex(block_hash_str)
@pytest.fixture
def block_header_str():
yield '0100000079c30d2c23727a1e9f5feda4e7feb8ea0bda2ab98e23e7f6a9cf594f00000000b0de897e42fa7a3b5c3a6bfb8e797acf4ffbc16169394b03ad93296524ed633dcfef6e49ffff001d36d19a6c'
@pytest.fixture
def block_header(block_header_str):
yield bytes.fromhex(block_header_str)
@pytest.fixture
def block_info(block_count, block_hash, block_header):
yield BlockInfo(
block_height=block_count,
block_hash=block_hash,
block_header=block_header,
)
class MockResponse:
def json(self):
return {}
@property
def status_code(self):
return 200
def raise_for_status(self):
pass
class MockGetCountResponse(MockResponse):
def __init__(self, block_count):
self.block_count = block_count
def json(self):
return {'result': '{}'.format(self.block_count)}
class MockGetBlockHashResponse(MockResponse):
def __init__(self, block_hash_str):
self.block_hash_str = block_hash_str
def json(self):
return {'result': '{}'.format(self.block_hash_str)}
class MockGetBlockHeaderResponse(MockResponse):
def __init__(self, block_header_str):
self.block_header_str = block_header_str
def json(self):
return {'result': '{}'.format(self.block_header_str)}
class MockEmptyResponse(MockResponse):
def json(self):
return {}
class MockInvalidStatusResponse(MockResponse):
def raise_for_status(self):
raise HTTPError("Some http error", response=self)
@pytest.fixture
def mock_get_count_response(block_count):
yield MockGetCountResponse(block_count)
@pytest.fixture
def mock_get_block_hash_response(block_hash_str):
yield MockGetBlockHashResponse(block_hash_str)
@pytest.fixture
def mock_get_block_header_response(block_header_str):
yield MockGetBlockHeaderResponse(block_header_str)
@pytest.fixture
def mock_empty_response():
yield MockEmptyResponse()
@pytest.fixture
def mock_invalid_status_response():
yield MockInvalidStatusResponse()
def test_get_block_count(bitcoin_core_client, mock_get_count_response, block_count):
with mock.patch('squeaknode.bitcoin.bitcoin_core_client.requests.post', autospec=True) as mock_post:
mock_post.return_value = mock_get_count_response
retrieved_block_count = bitcoin_core_client.get_block_count()
assert retrieved_block_count == block_count
def test_get_block_count_invalid_status(bitcoin_core_client, mock_invalid_status_response):
with mock.patch('squeaknode.bitcoin.bitcoin_core_client.requests.post', autospec=True) as mock_post:
mock_post.return_value = mock_invalid_status_response
with pytest.raises(BitcoinRequestError):
bitcoin_core_client.get_block_count()
def test_get_block_count_connection_error(bitcoin_core_client, mock_invalid_status_response):
with mock.patch('squeaknode.bitcoin.bitcoin_core_client.requests.post', autospec=True) as mock_post:
mock_post.side_effect = ConnectionError()
with pytest.raises(BitcoinRequestError):
bitcoin_core_client.get_block_count()
def test_get_block_count_timeout_error(bitcoin_core_client, mock_invalid_status_response):
with mock.patch('squeaknode.bitcoin.bitcoin_core_client.requests.post', autospec=True) as mock_post:
mock_post.side_effect = Timeout()
with pytest.raises(BitcoinRequestError):
bitcoin_core_client.get_block_count()
def test_get_block_count_request_exception(bitcoin_core_client, mock_invalid_status_response):
# TODO: assert mocks called with correct args.
with mock.patch('squeaknode.bitcoin.bitcoin_core_client.requests.post', autospec=True) as mock_post:
mock_post.side_effect = RequestException()
with pytest.raises(BitcoinRequestError):
bitcoin_core_client.get_block_count()
def test_get_block_hash(bitcoin_core_client, mock_get_block_hash_response, block_count, block_hash):
# TODO: assert mocks called with correct args.
with mock.patch('squeaknode.bitcoin.bitcoin_core_client.requests.post', autospec=True) as mock_post:
mock_post.return_value = mock_get_block_hash_response
retrieved_block_hash = bitcoin_core_client.get_block_hash(block_count)
assert retrieved_block_hash == block_hash
def test_get_block_header(bitcoin_core_client, mock_get_block_header_response, block_hash, block_header):
# TODO: assert mocks called with correct args.
with mock.patch('squeaknode.bitcoin.bitcoin_core_client.requests.post', autospec=True) as mock_post:
mock_post.return_value = mock_get_block_header_response
retrieved_block_header = bitcoin_core_client.get_block_header(
block_hash)
assert retrieved_block_header == block_header
def test_get_block_info_by_height(bitcoin_core_client, block_count, block_hash, block_header):
# TODO: assert mocks called with correct args.
with mock.patch.object(bitcoin_core_client, 'get_block_hash', autospec=True) as mock_get_block_hash, \
mock.patch.object(bitcoin_core_client, 'get_block_header', autospec=True) as mock_get_block_header:
mock_get_block_hash.return_value = block_hash
mock_get_block_header.return_value = block_header
retrieved_block_info = bitcoin_core_client.get_block_info_by_height(
block_count)
assert retrieved_block_info == BlockInfo(
block_height=block_count,
block_hash=block_hash,
block_header=block_header,
)
def test_get_best_block_info(bitcoin_core_client, block_count, block_info):
# TODO: assert mocks called with correct args.
with mock.patch.object(bitcoin_core_client, 'get_block_count', autospec=True) as mock_get_block_count, \
mock.patch.object(bitcoin_core_client, 'get_block_info_by_height', autospec=True) as mock_get_block_info_by_height:
mock_get_block_count.return_value = block_count
mock_get_block_info_by_height.return_value = block_info
retrieved_block_info = bitcoin_core_client.get_best_block_info()
assert retrieved_block_info == block_info