Test coverage and bug fixes for interactive client account selection with balances.

pull/1989/head
Kieran R. Prasch 2020-05-14 14:16:30 -07:00 committed by Kieran Prasch
parent 78ed8fd145
commit cc66d9310a
No known key found for this signature in database
GPG Key ID: 199AB839D4125A62
5 changed files with 203 additions and 12 deletions

View File

@ -80,6 +80,7 @@ def select_stake(stakeholder,
def select_client_account(emitter,
provider_uri: str = None,
signer: Signer = None,
signer_uri: str = None,
wallet: Wallet = None,
prompt: str = None,
@ -97,19 +98,31 @@ def select_client_account(emitter,
# We use Wallet internally as an account management abstraction
if not wallet:
if signer and signer_uri:
raise ValueError('Pass either signer or signer_uri but not both.')
if not provider_uri and not signer_uri:
raise ValueError("At least a provider URI or signer URI is necessary to select an account")
# Lazy connect the blockchain interface
if not BlockchainInterfaceFactory.is_interface_initialized(provider_uri=provider_uri):
BlockchainInterfaceFactory.initialize_interface(provider_uri=provider_uri, poa=poa, emitter=emitter)
signer = Signer.from_signer_uri(signer_uri) if signer_uri else None
if provider_uri:
# Lazy connect the blockchain interface
if not BlockchainInterfaceFactory.is_interface_initialized(provider_uri=provider_uri):
BlockchainInterfaceFactory.initialize_interface(provider_uri=provider_uri, poa=poa, emitter=emitter)
if signer_uri:
signer = Signer.from_signer_uri(signer_uri) if signer_uri else None
wallet = Wallet(provider_uri=provider_uri, signer=signer)
elif provider_uri or signer_uri:
raise ValueError("If you input a wallet, don't pass a provider URI or signer URI too")
# Display accounts info
if show_nu_balance or show_staking: # Lazy registry fetching
if not registry:
if not network:
raise ValueError("Pass network name or registry; Got neither.")
registry = InMemoryContractRegistry.from_latest_publication(network=network)
wallet_accounts = wallet.accounts

View File

@ -19,21 +19,19 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import tabulate
from web3.main import Web3
from nucypher.blockchain.eth.actors import StakeHolder
from nucypher.blockchain.eth.constants import STAKING_ESCROW_CONTRACT_NAME
from nucypher.blockchain.eth.token import NU
from nucypher.blockchain.eth.utils import datetime_at_period, prettify_eth_amount
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.cli.literature import NO_ACTIVE_STAKES, NO_STAKES_AT_ALL, NO_STAKING_ACCOUNTS, POST_STAKING_ADVICE
from nucypher.cli.literature import POST_STAKING_ADVICE
from nucypher.cli.painting.transactions import paint_receipt_summary
STAKE_TABLE_COLUMNS = ('Idx', 'Value', 'Remaining', 'Enactment', 'Termination')
STAKER_TABLE_COLUMNS = ('Status', 'Restaking', 'Winding Down', 'Unclaimed Fees', 'Min fee rate')
def paint_stakes(emitter: StdoutEmitter,
stakeholder: StakeHolder,
stakeholder: 'StakeHolder',
paint_inactive: bool = False,
staker_address: str = None) -> None:

View File

@ -0,0 +1,145 @@
from unittest.mock import Mock
import pytest
from eth_utils import is_checksum_address
from web3 import Web3
from nucypher.blockchain.eth.actors import Wallet
from nucypher.blockchain.eth.token import NU
from nucypher.cli.actions.select import select_client_account
from nucypher.config.constants import TEMPORARY_DOMAIN
from tests.constants import MOCK_PROVIDER_URI, MOCK_SIGNER_URI, NUMBER_OF_ETH_TEST_ACCOUNTS
def test_select_client_account(mock_click_prompt, test_emitter, mock_testerchain):
"""Fine-grained assertions about the return value of interactive client account selection"""
selection = 0
mock_click_prompt.return_value = selection
selected_account = select_client_account(emitter=test_emitter, provider_uri=MOCK_PROVIDER_URI)
assert selected_account, "Account selection returned Falsy instead of an address"
assert isinstance(selected_account, str), "Selection is not a str"
assert is_checksum_address(selected_account), "Selection is not a valid checksum address"
assert selected_account == mock_testerchain.etherbase_account, "Selection returned the wrong address"
def test_select_client_account_invalid_input(mock_click_prompt, test_emitter, mock_testerchain):
# Provider URI Problems
error_message = "At least a provider URI or signer URI is necessary to select an account"
with pytest.raises(ValueError, match=error_message):
select_client_account(emitter=test_emitter)
# Signer Problems
error_message = "Pass either signer or signer_uri but not both."
with pytest.raises(ValueError, match=error_message):
select_client_account(emitter=test_emitter, signer=Mock(), signer_uri=MOCK_SIGNER_URI)
def test_select_client_account_valid_inputs(mock_click_prompt,
test_emitter,
mock_testerchain,
patch_keystore,
mock_accounts):
selection = 0
mock_click_prompt.return_value = selection
# From Provider
selected_account = select_client_account(emitter=test_emitter, provider_uri=MOCK_PROVIDER_URI)
assert selected_account == mock_testerchain.etherbase_account
# From Wallet
wallet = Wallet(provider_uri=MOCK_PROVIDER_URI)
selected_account = select_client_account(emitter=test_emitter, wallet=wallet)
assert selected_account == mock_testerchain.etherbase_account
# From External Signer
selected_account = select_client_account(emitter=test_emitter, signer_uri=MOCK_SIGNER_URI)
signer_etherbase_keystore = list(mock_accounts.items())[0]
_filename, signer_etherbase_account = signer_etherbase_keystore
assert selected_account == signer_etherbase_account.address
@pytest.mark.parametrize('selection,show_staking,show_eth,show_tokens,mock_stakes',(
(0, True, True, True, []),
(1, True, True, True, []),
(5, True, True, True, []),
(NUMBER_OF_ETH_TEST_ACCOUNTS-1, True, True, True, []),
(4, True, True, True, [(1, 2, 3)]),
(0, False, True, True, []),
(0, False, False, True, []),
(0, False, False, False, []),
))
def test_select_client_account_with_balance_display(mock_click_prompt,
test_emitter,
mock_testerchain,
stdout_trap,
test_registry_source_manager,
mock_staking_agent,
mock_token_agent,
selection,
show_staking,
show_eth,
show_tokens,
mock_stakes):
mock_click_prompt.return_value = selection
# Missing network kwarg with balance display active
blockchain_read_required = any((show_staking, show_eth, show_tokens))
if blockchain_read_required:
with pytest.raises(ValueError, match='Pass network name or registry; Got neither.'):
select_client_account(emitter=test_emitter,
show_eth_balance=show_eth,
show_nu_balance=show_tokens,
show_staking=show_staking,
provider_uri=MOCK_PROVIDER_URI)
mock_staking_agent.get_all_stakes.return_value = mock_stakes
selected_account = select_client_account(emitter=test_emitter,
network=TEMPORARY_DOMAIN,
show_eth_balance=show_eth,
show_nu_balance=show_tokens,
show_staking=show_staking,
provider_uri=MOCK_PROVIDER_URI)
# check for accurate selection consistency with client index
assert selected_account == mock_testerchain.client.accounts[selection]
# Display account info
headers = ['Account']
if show_staking:
headers.append('Staking')
if show_eth:
headers.append('ETH')
if show_tokens:
headers.append('NU')
raw_output = stdout_trap.getvalue()
for column_name in headers:
assert column_name in raw_output, f'"{column_name}" column was not displayed'
output = raw_output.strip().split('\n')
body = output[2:-1]
assert len(body) == len(mock_testerchain.client.accounts), "Some accounts are not displayed"
accounts = dict()
for row in body:
account_display = row.split()
account_data = dict(zip(headers, account_display[1::]))
account = account_data['Account']
accounts[account] = account_data
assert is_checksum_address(account)
if show_tokens:
balance = mock_token_agent.get_balance(address=account)
assert str(NU.from_nunits(balance)) in row
if show_eth:
balance = mock_testerchain.client.get_balance(account=account)
assert str(Web3.fromWei(balance, 'ether')) in row
if show_staking:
if len(mock_stakes) == 0:
assert "No" in row
else:
assert 'Yes' in row

View File

@ -23,14 +23,15 @@ from eth_account.account import Account
from io import StringIO
from nucypher.blockchain.economics import EconomicsFactory
from nucypher.blockchain.eth import KeystoreSigner
from nucypher.blockchain.eth.agents import ContractAgency
from nucypher.blockchain.eth.interfaces import BlockchainInterface, BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.config.characters import UrsulaConfiguration
from tests.constants import KEYFILE_NAME_TEMPLATE, NUMBER_OF_ETH_TEST_ACCOUNTS
from tests.constants import KEYFILE_NAME_TEMPLATE, MOCK_KEYSTORE_PATH, NUMBER_OF_ETH_TEST_ACCOUNTS
from tests.fixtures import _make_testerchain, make_token_economics
from tests.mock.agents import FAKE_RECEIPT, MockContractAgency, MockStakingAgent, MockWorkLockAgent
from tests.mock.agents import FAKE_RECEIPT, MockContractAgency, MockNucypherToken, MockStakingAgent, MockWorkLockAgent
from tests.mock.interfaces import MockBlockchain, make_mock_registry_source_manager
@ -50,6 +51,13 @@ def mock_contract_agency(monkeymodule, module_mocker, token_economics):
monkeymodule.delattr(ContractAgency, 'get_agent')
@pytest.fixture(scope='module', autouse=True)
def mock_token_agent(mock_testerchain, token_economics, mock_contract_agency):
mock_agent = mock_contract_agency.get_agent(MockNucypherToken)
yield mock_agent
mock_agent.reset()
@pytest.fixture(scope='module', autouse=True)
def mock_worklock_agent(mock_testerchain, token_economics, mock_contract_agency):
mock_agent = mock_contract_agency.get_agent(MockWorkLockAgent)
@ -74,10 +82,11 @@ def mock_click_confirm(mocker):
return mocker.patch.object(click, 'confirm')
@pytest.fixture()
@pytest.fixture(scope='function')
def stdout_trap():
trap = StringIO()
return trap
yield trap
trap.truncate(0)
@pytest.fixture()
@ -165,3 +174,26 @@ def worker_address(worker_account):
def custom_config_filepath(custom_filepath):
filepath = os.path.join(custom_filepath, UrsulaConfiguration.generate_filename())
return filepath
@pytest.fixture(scope='function')
def patch_keystore(mock_accounts, monkeypatch, mocker):
def successful_mock_keyfile_reader(_keystore, path):
# Ensure the absolute path is passed to the keyfile reader
assert MOCK_KEYSTORE_PATH in path
full_path = path
del path
for filename, account in mock_accounts.items(): # Walk the mock filesystem
if filename in full_path:
break
else:
raise FileNotFoundError(f"No such file {full_path}")
return account.address, dict(version=3, address=account.address)
mocker.patch('os.listdir', return_value=list(mock_accounts.keys()))
monkeypatch.setattr(KeystoreSigner, '_KeystoreSigner__read_keyfile', successful_mock_keyfile_reader)
yield
monkeypatch.delattr(KeystoreSigner, '_KeystoreSigner__read_keyfile')

View File

@ -150,6 +150,9 @@ class MockContractAgent:
class MockNucypherToken(MockContractAgent, NucypherTokenAgent):
"""Look at me im a token!"""
CALLS = ('get_balance',
)
class MockStakingAgent(MockContractAgent, StakingEscrowAgent):
"""dont forget the eggs!"""