From a01c0e6672ed8aa0589fa8c36255cae64eaf2010 Mon Sep 17 00:00:00 2001 From: Markus Levonyak Date: Mon, 25 Nov 2024 03:38:41 +0000 Subject: [PATCH] fix: node registration prerequisites checks --- pantos/servicenode/blockchains/base.py | 53 +++++++++ pantos/servicenode/blockchains/ethereum.py | 14 +++ pantos/servicenode/blockchains/solana.py | 8 ++ pantos/servicenode/business/base.py | 48 ++++++++- pantos/servicenode/business/bids.py | 7 +- pantos/servicenode/business/node.py | 64 +++++++++-- pantos/servicenode/business/plugins.py | 5 + pantos/servicenode/business/transfers.py | 19 ++-- tests/blockchains/test_base.py | 32 ++++++ tests/blockchains/test_ethereum.py | 34 ++++++ tests/business/test_node.py | 118 +++++++++++++++++++-- 11 files changed, 377 insertions(+), 25 deletions(-) diff --git a/pantos/servicenode/blockchains/base.py b/pantos/servicenode/blockchains/base.py index 5e04bce..e7510a2 100644 --- a/pantos/servicenode/blockchains/base.py +++ b/pantos/servicenode/blockchains/base.py @@ -114,6 +114,18 @@ def __init__(self): f'unable to initialize the {self.get_blockchain_name()} ' 'utilities') + @abc.abstractmethod + def get_own_address(self) -> BlockchainAddress: + """Get the service node's own address on the blockchain. + + Returns + ------- + BlockchainAddress + The service node's own address. + + """ + pass # pragma: no cover + @abc.abstractmethod def is_node_registered(self) -> bool: """Determine if the service node is registered at the Pantos Hub @@ -171,6 +183,24 @@ def is_valid_recipient_address(self, recipient_address: str) -> bool: """ pass # pragma: no cover + @abc.abstractmethod + def read_minimum_deposit(self) -> int: + """Read the service node's minimum deposit at the Pantos Hub on + the blockchain. + + Returns + ------- + int + The service node's minimum deposit. + + Raises + ------ + BlockchainClientError + If the service node's minimum deposit cannot be read. + + """ + pass # pragma: no cover + @abc.abstractmethod def read_node_url(self) -> str: """Read the service node's URL that is registered at the Pantos @@ -189,6 +219,29 @@ def read_node_url(self) -> str: """ pass # pragma: no cover + def read_own_pan_balance(self) -> int: + """Read the service node's own PAN token balance on the + blockchain. + + Returns + ------- + int + The service node's own PAN token balance. + + Raises + ------ + BlockchainClientError + If the service node's own PAN token balance cannot be read. + + """ + try: + return self._get_utilities().get_balance( + self.get_own_address(), + token_address=self._get_config()['pan_token']) + except Exception: + raise self._create_error( + 'unable to read the own PAN token balance') + @abc.abstractmethod def register_node(self, node_url: str, node_deposit: int, withdrawal_address: BlockchainAddress) -> None: diff --git a/pantos/servicenode/blockchains/ethereum.py b/pantos/servicenode/blockchains/ethereum.py index 6267fb5..41bb6ef 100644 --- a/pantos/servicenode/blockchains/ethereum.py +++ b/pantos/servicenode/blockchains/ethereum.py @@ -79,6 +79,10 @@ def get_error_class(cls) -> type[BlockchainClientError]: # Docstring inherited return EthereumClientError + def get_own_address(self) -> BlockchainAddress: + # Docstring inherited + return self.__address # pragma: no cover + def is_node_registered(self) -> bool: # Docstring inherited try: @@ -101,6 +105,16 @@ def is_valid_recipient_address(self, recipient_address: str) -> bool: is_zero_address = int(recipient_address, 0) == 0 return not is_zero_address + def read_minimum_deposit(self) -> int: + # Docstring inherited + try: + node_connections = self.__create_node_connections() + hub_contract = self._create_hub_contract(node_connections) + return hub_contract.caller().\ + getCurrentMinimumServiceNodeDeposit().get() + except Exception: + raise self._create_error('unable to read the minimum deposit') + def read_node_url(self) -> str: # Docstring inherited try: diff --git a/pantos/servicenode/blockchains/solana.py b/pantos/servicenode/blockchains/solana.py index dcfc4f1..261024b 100644 --- a/pantos/servicenode/blockchains/solana.py +++ b/pantos/servicenode/blockchains/solana.py @@ -35,6 +35,10 @@ def get_error_class(cls) -> type[BlockchainClientError]: # Docstring inherited return SolanaClientError + def get_own_address(self) -> BlockchainAddress: + # Docstring inherited + raise NotImplementedError # pragma: no cover + def is_node_registered(self) -> bool: # Docstring inherited return False # pragma: no cover @@ -43,6 +47,10 @@ def is_valid_recipient_address(self, recipient_address: str) -> bool: # Docstring inherited raise NotImplementedError # pragma: no cover + def read_minimum_deposit(self) -> int: + # Docstring inherited + raise NotImplementedError # pragma: no cover + def read_node_url(self) -> str: # Docstring inherited raise NotImplementedError # pragma: no cover diff --git a/pantos/servicenode/business/base.py b/pantos/servicenode/business/base.py index 6726b7c..99491e1 100644 --- a/pantos/servicenode/business/base.py +++ b/pantos/servicenode/business/base.py @@ -1,7 +1,9 @@ """Base classes for all business logic interactors and errors. """ -import abc +import typing + +from pantos.common.exceptions import ErrorCreator from pantos.servicenode.exceptions import ServiceNodeError @@ -13,8 +15,48 @@ class InteractorError(ServiceNodeError): pass -class Interactor(abc.ABC): +class InvalidAmountError(InteractorError): + """Exception to be raised if an amount is invalid. + + """ + def __init__(self, **kwargs: typing.Any): + # Docstring inherited + super().__init__('invalid amount', **kwargs) + + +class InvalidBlockchainAddressError(InteractorError): + """Exception to be raised if a blockchain address is invalid. + + """ + def __init__(self, **kwargs: typing.Any): + # Docstring inherited + super().__init__('invalid blockchain address', **kwargs) + + +class InvalidUrlError(InteractorError): + """Exception to be raised if a URL is invalid. + + """ + def __init__(self, **kwargs: typing.Any): + # Docstring inherited + super().__init__('invalid URL', **kwargs) + + +class Interactor(ErrorCreator[InteractorError]): """Base class for all interactors. """ - pass + def _create_invalid_amount_error(self, + **kwargs: typing.Any) -> InteractorError: + return self._create_error(specialized_error_class=InvalidAmountError, + **kwargs) + + def _create_invalid_blockchain_address_error( + self, **kwargs: typing.Any) -> InteractorError: + return self._create_error( + specialized_error_class=InvalidBlockchainAddressError, **kwargs) + + def _create_invalid_url_error(self, + **kwargs: typing.Any) -> InteractorError: + return self._create_error(specialized_error_class=InvalidUrlError, + **kwargs) diff --git a/pantos/servicenode/business/bids.py b/pantos/servicenode/business/bids.py index 17af248..182b068 100644 --- a/pantos/servicenode/business/bids.py +++ b/pantos/servicenode/business/bids.py @@ -25,6 +25,11 @@ class BidInteractor(Interactor): """Interactor for managing service node bids. """ + @classmethod + def get_error_class(cls) -> type[InteractorError]: + # Docstring inherited + return BidInteractorError + def get_current_bids( self, source_blockchain_id: int, destination_blockchain_id: int) -> list[dict[str, int | str]]: @@ -76,7 +81,7 @@ def get_current_bids( 'signature': signature }) except Exception: - raise BidInteractorError( + raise self._create_error( 'unable to get the current bids', source_blockchain_id=source_blockchain_id, destination_blockchain_id=destination_blockchain_id) diff --git a/pantos/servicenode/business/node.py b/pantos/servicenode/business/node.py index 0ae6ad9..9d46985 100644 --- a/pantos/servicenode/business/node.py +++ b/pantos/servicenode/business/node.py @@ -2,15 +2,19 @@ """ import logging +import urllib.parse from pantos.common.blockchains.enums import Blockchain +from pantos.servicenode.blockchains.base import BlockchainClient from pantos.servicenode.blockchains.factory import get_blockchain_client from pantos.servicenode.business.base import Interactor from pantos.servicenode.business.base import InteractorError from pantos.servicenode.configuration import config from pantos.servicenode.configuration import get_blockchain_config +_VALID_NODE_URL_SCHEMES = ['http', 'https'] + _logger = logging.getLogger(__name__) """Logger for this module.""" @@ -26,14 +30,28 @@ class NodeInteractor(Interactor): """Interactor for managing the service node itself. """ + @classmethod + def get_error_class(cls) -> type[InteractorError]: + # Docstring inherited + return NodeInteractorError + def update_node_registrations(self) -> None: """Update the service node registrations on all supported blockchains. Raises ------ + InvalidUrlError + If the configured service node URL is invalid. + InvalidAmountError + If the configured service node deposit is less than the + Pantos Hub's minimum service node deposit or greater than + the service node's own PAN token balance. + InvalidBlockchainAddressError + If the configured withdrawal address is invalid. NodeInteractorError - If a service node registration cannot be updated. + If a service node registration cannot be updated for any + other reason. """ for blockchain in Blockchain: @@ -50,6 +68,7 @@ def update_node_registrations(self) -> None: old_node_url = blockchain_client.read_node_url() new_node_url = config['application']['url'] if old_node_url != new_node_url: + self.__validate_node_url(new_node_url) blockchain_client.update_node_url(new_node_url) elif to_be_registered: is_unbonding = blockchain_client.is_unbonding() @@ -59,17 +78,50 @@ def update_node_registrations(self) -> None: blockchain_client.cancel_unregistration() else: # Not yet registered - withdrawal_address = blockchain_config[ - 'withdrawal_address'] node_url = config['application']['url'] node_deposit = blockchain_config['deposit'] + withdrawal_address = blockchain_config[ + 'withdrawal_address'] + self.__validate_node_url(node_url) + self.__validate_node_deposit(blockchain_client, + node_deposit) + self.__validate_withdrawal_address( + blockchain_client, withdrawal_address) blockchain_client.register_node( node_url, node_deposit, withdrawal_address) elif is_registered: # Not to be registered anymore blockchain_client.unregister_node() # Do nothing if neither registered nor to be registered + except NodeInteractorError: + raise except Exception: - raise NodeInteractorError( - 'unable to update the service node registration on ' - '{}'.format(blockchain.name)) + raise self._create_error( + 'unable to update a service node registration', + blockchain=blockchain) + + def __validate_node_deposit(self, blockchain_client: BlockchainClient, + node_deposit: int) -> None: + minimum_deposit = blockchain_client.read_minimum_deposit() + own_pan_balance = blockchain_client.read_own_pan_balance() + if node_deposit < minimum_deposit or node_deposit > own_pan_balance: + raise self._create_invalid_amount_error( + node_deposit=node_deposit, minimum_deposit=minimum_deposit, + own_pan_balance=own_pan_balance) + + def __validate_node_url(self, node_url: str) -> None: + try: + parse_result = urllib.parse.urlparse(node_url) + except ValueError: + raise self._create_invalid_url_error(node_url=node_url) + is_scheme_valid = parse_result.scheme in _VALID_NODE_URL_SCHEMES + is_netloc_valid = len(parse_result.netloc) > 0 + if not is_scheme_valid or not is_netloc_valid: + raise self._create_invalid_url_error(node_url=node_url) + + def __validate_withdrawal_address(self, + blockchain_client: BlockchainClient, + withdrawal_address: str) -> None: + if not blockchain_client.is_valid_address(withdrawal_address): + raise self._create_invalid_blockchain_address_error( + withdrawal_address=withdrawal_address) diff --git a/pantos/servicenode/business/plugins.py b/pantos/servicenode/business/plugins.py index 5960a78..4be68c2 100644 --- a/pantos/servicenode/business/plugins.py +++ b/pantos/servicenode/business/plugins.py @@ -32,6 +32,11 @@ class BidPluginInteractor(Interactor): """Interactor for handling the bid plugin operations. """ + @classmethod + def get_error_class(cls) -> type[InteractorError]: + # Docstring inherited + return BidPluginInteractorError + def replace_bids(self, source_blockchain: Blockchain) -> int: """Replace the old bids with new bids given by the bid plugin. Additionally, the Validator fee is added to the bid fee. diff --git a/pantos/servicenode/business/transfers.py b/pantos/servicenode/business/transfers.py index bcb23c4..1a6be21 100644 --- a/pantos/servicenode/business/transfers.py +++ b/pantos/servicenode/business/transfers.py @@ -69,6 +69,11 @@ class TransferInteractor(Interactor): """Interactor for handling token transfers. """ + @classmethod + def get_error_class(cls) -> type[InteractorError]: + # Docstring inherited + return TransferInteractorError + @dataclasses.dataclass class ConfirmTransferRequest: """Request data for confirming the inclusion of a token transfer @@ -158,7 +163,7 @@ def confirm_transfer(self, request: ConfirmTransferRequest) -> bool: request.internal_transfer_id, TransferStatus.CONFIRMED) return True except Exception: - raise TransferInteractorError( + raise self._create_error( 'unable to determine if a token transfer is confirmed', request=request) @@ -250,8 +255,8 @@ def execute_transfer(self, request: ExecuteTransferRequest) -> uuid.UUID: except TransferInteractorUnrecoverableError: raise except Exception: - raise TransferInteractorError('unable to execute a token transfer', - request=request) + raise self._create_error('unable to execute a token transfer', + request=request) def __single_chain_transfer(self, request: ExecuteTransferRequest) -> uuid.UUID: @@ -634,8 +639,8 @@ def find_transfer(self, task_id: uuid.UUID) -> FindTransferResponse: except TransferInteractorResourceNotFoundError: raise except Exception: - raise TransferInteractorError( - 'unable to search for a token transfer', task_id=task_id) + raise self._create_error('unable to search for a token transfer', + task_id=task_id) @dataclasses.dataclass class InitiateTransferRequest: @@ -772,8 +777,8 @@ def initiate_transfer(self, request: InitiateTransferRequest) -> uuid.UUID: except TransferInteractorBidNotAcceptedError: raise except Exception: - raise TransferInteractorError( - 'unable to initiate a new token transfer', request=request) + raise self._create_error('unable to initiate a new token transfer', + request=request) @celery.current_app.task(bind=True, max_retries=100) diff --git a/tests/blockchains/test_base.py b/tests/blockchains/test_base.py index bea6503..7e5db6f 100644 --- a/tests/blockchains/test_base.py +++ b/tests/blockchains/test_base.py @@ -19,6 +19,7 @@ 'average_block_time': 14, 'confirmations': 12, 'chain_id': 1, + 'pan_token': '0xcd8fa68c471d7703C074EA5e2C56B852795B33c0', 'private_key': tempfile.mkstemp()[1], 'private_key_password': 'some_password' } @@ -32,6 +33,10 @@ _ON_CHAIN_TRANSFER_ID = 10512 +_OWN_ADDRESS = '0x7De6Ce2Ce98B446CdD2730d2D49B0e1FEe2Ff85C' + +_OWN_PAN_BALANCE = 10**5 * 10**8 + @pytest.fixture(scope='module') @unittest.mock.patch( @@ -83,6 +88,33 @@ def test_init_error(mock_get_blockchain, mock_get_config, mock_create_error, BlockchainUtilitiesError) +@unittest.mock.patch.object(BlockchainClient, 'get_own_address', + return_value=_OWN_ADDRESS) +@unittest.mock.patch.object(BlockchainClient, '_get_utilities') +@unittest.mock.patch.object(BlockchainClient, '_get_config', + return_value=_MOCK_CONFIG) +def test_read_own_pan_balance_correct(mock_get_config, mock_get_utilities, + mock_get_own_address, blockchain_client): + mock_get_utilities().get_balance.return_value = _OWN_PAN_BALANCE + own_pan_balance = blockchain_client.read_own_pan_balance() + assert own_pan_balance == _OWN_PAN_BALANCE + + +@unittest.mock.patch.object(BlockchainClient, 'get_own_address', + return_value=_OWN_ADDRESS) +@unittest.mock.patch.object(BlockchainClient, '_get_utilities') +@unittest.mock.patch.object(BlockchainClient, '_get_config', + return_value=_MOCK_CONFIG) +@unittest.mock.patch.object(BlockchainClient, 'get_error_class', + return_value=BlockchainClientError) +def test_read_own_pan_balance_error(mock_get_error_class, mock_get_config, + mock_get_utilities, mock_get_own_address, + blockchain_client): + mock_get_utilities().get_balance.side_effect = BlockchainUtilitiesError('') + with pytest.raises(BlockchainClientError): + blockchain_client.read_own_pan_balance() + + @unittest.mock.patch.object(BlockchainClient, '_get_utilities') def test_get_transfer_submission_status_not_completed(mock_get_utilities, blockchain_client): diff --git a/tests/blockchains/test_ethereum.py b/tests/blockchains/test_ethereum.py index ab9d48b..0dbb555 100644 --- a/tests/blockchains/test_ethereum.py +++ b/tests/blockchains/test_ethereum.py @@ -45,6 +45,8 @@ _SERVICE_NODE_URL = 'servicenode.pantos.testurl' +_MINIMUM_DEPOSIT = 10**5 * 10**8 + _DESTINATION_BLOCKCHAIN = Blockchain.BNB_CHAIN _TRANSFER_INTERNAL_ID = 7 @@ -331,6 +333,38 @@ def test_is_valid_recipient_address_0_address_false(mock_is_valid_address, assert is_recipient_address_correct is False +def test_read_minimum_deposit_correct(ethereum_client, + mock_get_blockchain_config, + provider_timeout, hub_contract_address, + mock_get_blockchain_utilities): + mock_get_blockchain_config.return_value = { + 'provider_timeout': provider_timeout, + 'hub': hub_contract_address + } + mock_get_blockchain_utilities().create_contract().caller().\ + getCurrentMinimumServiceNodeDeposit().get.return_value = \ + _MINIMUM_DEPOSIT + + minimum_deposit = ethereum_client.read_minimum_deposit() + + assert minimum_deposit == _MINIMUM_DEPOSIT + + +def test_read_minimum_deposit_error(ethereum_client, + mock_get_blockchain_config, + provider_timeout, hub_contract_address, + mock_get_blockchain_utilities): + mock_get_blockchain_config.return_value = { + 'provider_timeout': provider_timeout, + 'hub': hub_contract_address + } + mock_get_blockchain_utilities().create_contract().caller.side_effect = \ + EthereumUtilitiesError + + with pytest.raises(EthereumClientError): + ethereum_client.read_minimum_deposit() + + def test_read_node_url_correct(ethereum_client, mock_get_blockchain_config, provider_timeout, hub_contract_address, mock_get_blockchain_utilities, diff --git a/tests/business/test_node.py b/tests/business/test_node.py index d5697c8..201f60b 100644 --- a/tests/business/test_node.py +++ b/tests/business/test_node.py @@ -6,6 +6,9 @@ from pantos.servicenode.blockchains.base import BlockchainClientError from pantos.servicenode.business import node as node_module +from pantos.servicenode.business.base import InvalidAmountError +from pantos.servicenode.business.base import InvalidBlockchainAddressError +from pantos.servicenode.business.base import InvalidUrlError from pantos.servicenode.business.node import NodeInteractor from pantos.servicenode.business.node import NodeInteractorError @@ -17,6 +20,12 @@ _CONFIG_WITHDRAWAL_ADDRESS = '0xceb95Cb81e4f71c8Fc426a84fA29F2ac552AD752' +_DEFAULT_MINIMUM_DEPOSIT = 10000000000000 + +_CONFIG_DEPOSIT = _DEFAULT_MINIMUM_DEPOSIT + +_DEFAULT_OWN_PAN_BALANCE = _DEFAULT_MINIMUM_DEPOSIT + class MockBlockchainClientError(BlockchainClientError): def __init__(self): @@ -24,24 +33,44 @@ def __init__(self): class MockBlockchainClient: - def __init__(self, node_registered, is_unbonding, raise_error): + def __init__(self, node_registered, is_unbonding, raise_error, + minimum_deposit=None, own_pan_balance=None): self.node_registered = node_registered self.unbonding = is_unbonding self.node_url = _DEFAULT_NODE_URL self.withdrawal_address = _DEFAULT_WITHDRAWAL_ADDRESS self.raise_error = raise_error + self.minimum_deposit = (minimum_deposit if minimum_deposit is not None + else _DEFAULT_MINIMUM_DEPOSIT) + self.own_pan_balance = (own_pan_balance if own_pan_balance is not None + else _DEFAULT_OWN_PAN_BALANCE) def is_node_registered(self): if self.raise_error: raise MockBlockchainClientError return self.node_registered + def is_valid_address(self, address): + if self.raise_error: + raise MockBlockchainClientError + return len(address) > 0 + + def read_minimum_deposit(self): + if self.raise_error: + raise MockBlockchainClientError + return self.minimum_deposit + def read_node_url(self): if self.raise_error: raise MockBlockchainClientError assert self.node_registered return self.node_url + def read_own_pan_balance(self): + if self.raise_error: + raise MockBlockchainClientError + return self.own_pan_balance + def register_node(self, node_url, node_deposit, withdrawal_address): if self.raise_error: raise MockBlockchainClientError @@ -49,7 +78,7 @@ def register_node(self, node_url, node_deposit, withdrawal_address): assert isinstance(node_url, str) assert len(node_url) > 0 assert isinstance(node_deposit, int) - assert node_deposit >= 0 + assert node_deposit >= self.minimum_deposit self.node_registered = True self.node_url = node_url self.withdrawal_address = withdrawal_address @@ -83,11 +112,17 @@ def cancel_unregistration(self): @pytest.fixture(autouse=True) def mock_blockchain_client(request, monkeypatch): is_registered_marker = request.node.get_closest_marker('is_registered') - unbonding_marker = request.node.get_closest_marker('unbonding') - unbonding = [] if unbonding_marker is None else unbonding_marker.args[0] is_registered = ([] if is_registered_marker is None else is_registered_marker.args[0]) + unbonding_marker = request.node.get_closest_marker('unbonding') + unbonding = [] if unbonding_marker is None else unbonding_marker.args[0] error_marker = request.node.get_closest_marker('error') + minimum_deposit_marker = request.node.get_closest_marker('minimum_deposit') + minimum_deposit = (None if minimum_deposit_marker is None else + minimum_deposit_marker.args[0]) + own_pan_balance_marker = request.node.get_closest_marker('own_pan_balance') + own_pan_balance = (None if own_pan_balance_marker is None else + own_pan_balance_marker.args[0]) blockchain_clients: dict[Blockchain, MockBlockchainClient] = {} def mock_get_blockchain_client(blockchain): @@ -96,7 +131,8 @@ def mock_get_blockchain_client(blockchain): except KeyError: blockchain_client = MockBlockchainClient( blockchain in is_registered, blockchain in unbonding, - error_marker is not None) + error_marker is not None, minimum_deposit=minimum_deposit, + own_pan_balance=own_pan_balance) blockchain_clients[blockchain] = blockchain_client return blockchain_client @@ -110,9 +146,17 @@ def mock_config(request, monkeypatch): 'to_be_registered') to_be_registered = ([] if to_be_registered_marker is None else to_be_registered_marker.args[0]) + node_url_marker = request.node.get_closest_marker('node_url') + node_url = (_CONFIG_NODE_URL + if node_url_marker is None else node_url_marker.args[0]) + withdrawal_address_marker = request.node.get_closest_marker( + 'withdrawal_address') + withdrawal_address = (_CONFIG_WITHDRAWAL_ADDRESS + if withdrawal_address_marker is None else + withdrawal_address_marker.args[0]) config: dict[str, typing.Any] = {} config['application'] = {} - config['application']['url'] = _CONFIG_NODE_URL + config['application']['url'] = node_url config['blockchains'] = {} for blockchain in Blockchain: blockchain_name = blockchain.name.lower() @@ -120,9 +164,9 @@ def mock_config(request, monkeypatch): config['blockchains'][blockchain_name]['active'] = True config['blockchains'][blockchain_name]['registered'] = ( blockchain in to_be_registered) - config['blockchains'][blockchain_name]['deposit'] = 10000000000000 + config['blockchains'][blockchain_name]['deposit'] = _CONFIG_DEPOSIT config['blockchains'][blockchain_name][ - 'withdrawal_address'] = _CONFIG_WITHDRAWAL_ADDRESS + 'withdrawal_address'] = withdrawal_address def mock_get_blockchain_config(blockchain): return config['blockchains'][blockchain.name.lower()] @@ -230,6 +274,64 @@ def test_update_node_registrations_some_active_some_registered_non_overlapping( update_node_registrations_no_error(request, node_interactor) +@pytest.mark.to_be_registered(list(Blockchain)) +@pytest.mark.node_url('ftp://some.url') +def test_update_node_registrations_invalid_node_url_error(node_interactor): + with pytest.raises(InvalidUrlError) as exception_info: + node_interactor.update_node_registrations() + + assert isinstance(exception_info.value, NodeInteractorError) + + +@pytest.mark.to_be_registered(list(Blockchain)) +@pytest.mark.is_registered(list(Blockchain)) +@pytest.mark.node_url('nourl') +def test_update_node_registrations_invalid_node_url_update_error( + node_interactor): + with pytest.raises(InvalidUrlError) as exception_info: + node_interactor.update_node_registrations() + + assert isinstance(exception_info.value, NodeInteractorError) + + +@pytest.mark.to_be_registered(list(Blockchain)) +@unittest.mock.patch('urllib.parse.urlparse', side_effect=ValueError) +def test_update_node_registrations_invalid_node_url_urlparse_error( + mock_urlparse, node_interactor): + with pytest.raises(InvalidUrlError) as exception_info: + node_interactor.update_node_registrations() + + assert isinstance(exception_info.value, NodeInteractorError) + + +@pytest.mark.to_be_registered(list(Blockchain)) +@pytest.mark.minimum_deposit(_CONFIG_DEPOSIT + 1) +def test_update_node_registrations_invalid_deposit_error(node_interactor): + with pytest.raises(InvalidAmountError) as exception_info: + node_interactor.update_node_registrations() + + assert isinstance(exception_info.value, NodeInteractorError) + + +@pytest.mark.to_be_registered(list(Blockchain)) +@pytest.mark.own_pan_balance(_CONFIG_DEPOSIT - 1) +def test_update_node_registrations_invalid_balance_error(node_interactor): + with pytest.raises(InvalidAmountError) as exception_info: + node_interactor.update_node_registrations() + + assert isinstance(exception_info.value, NodeInteractorError) + + +@pytest.mark.to_be_registered(list(Blockchain)) +@pytest.mark.withdrawal_address('') +def test_update_node_registrations_invalid_withdrawal_address_error( + node_interactor): + with pytest.raises(InvalidBlockchainAddressError) as exception_info: + node_interactor.update_node_registrations() + + assert isinstance(exception_info.value, NodeInteractorError) + + @pytest.mark.to_be_registered(list(Blockchain)) @pytest.mark.error def test_update_node_registrations_error(node_interactor):