mirror of
https://github.com/yzernik/squeaknode.git
synced 2026-08-17 13:07:32 +02:00
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:
parent
7a88fd95ed
commit
e0c63e2927
4 changed files with 308 additions and 31 deletions
|
|
@ -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()
|
||||
37
squeaknode/bitcoin/exception.py
Normal file
37
squeaknode/bitcoin/exception.py
Normal 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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
240
tests/bitcoin/test_bitcoin_core_client.py
Normal file
240
tests/bitcoin/test_bitcoin_core_client.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue