CLI config action test coverage and related bug fixes.

pull/1989/head
Kieran Prasch 2020-05-16 21:21:14 -07:00
parent b9de67505d
commit 4a5aed06a1
No known key found for this signature in database
GPG Key ID: 199AB839D4125A62
9 changed files with 165 additions and 68 deletions

View File

@ -24,8 +24,8 @@ from json.decoder import JSONDecodeError
from typing import Type
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.cli.actions.confirm import confirm_destroy_configuration
from nucypher.cli.literature import (
CHARACTER_DESTRUCTION,
CONFIRM_FORGET_NODES,
INVALID_CONFIGURATION_FILE_WARNING,
INVALID_JSON_IN_CONFIGURATION_WARNING,
@ -47,19 +47,20 @@ def forget(emitter: StdoutEmitter, configuration: CharacterConfiguration) -> Non
def update_configuration(emitter: StdoutEmitter,
config_class: Type[CharacterConfiguration],
filepath: str,
config_options) -> None:
updates: dict) -> None:
"""
Writes updates to an existing configuration file then display the result.
Utility for writing updates to an existing configuration file then displaying the result.
If the config file is invalid, trey very hard to display the problem.
"""
try:
config = config_class.from_configuration_file(filepath=filepath)
except config_class.ConfigurationError:
return handle_invalid_configuration_file(emitter=emitter, config_class=config_class, filepath=filepath)
updates = config_options.get_updates()
if updates:
emitter.message(SUCCESSFUL_UPDATE_CONFIGURATION_VALUES.format(fields=', '.join(updates)), color='yellow')
config.update(**updates)
return handle_invalid_configuration_file(emitter=emitter,
config_class=config_class,
filepath=filepath)
pretty_fields = ', '.join(updates)
emitter.message(SUCCESSFUL_UPDATE_CONFIGURATION_VALUES.format(fields=pretty_fields), color='yellow')
config.update(**updates)
emitter.echo(config.serialize())
@ -68,21 +69,7 @@ def destroy_configuration(emitter: StdoutEmitter,
force: bool = False) -> None:
"""Destroy a character configuration and report rhe result with an emitter."""
if not force:
################################
# TODO: This is a workaround for ursula - needs follow up
try:
database = character_config.db_filepath
except AttributeError:
database = "No database found" # FIXME: This cannot be right.....
################################
click.confirm(CHARACTER_DESTRUCTION.format(name=character_config.NAME,
root=character_config.config_root,
keystore=character_config.keyring_root,
nodestore=character_config.node_storage.root_dir,
config=character_config.filepath,
database=database), abort=True)
confirm_destroy_configuration(config=character_config)
character_config.destroy()
emitter.message(SUCCESSFUL_DESTRUCTION, color='green')
character_config.log.debug(SUCCESSFUL_DESTRUCTION)
@ -111,14 +98,15 @@ def handle_invalid_configuration_file(emitter: StdoutEmitter,
try:
# ... but try to display it anyways
response = config_class._read_configuration_file(filepath=filepath)
return emitter.echo(json.dumps(response, indent=4))
except JSONDecodeError:
emitter.echo(json.dumps(response, indent=4))
raise config_class.ConfigurationError
except (TypeError, JSONDecodeError):
emitter.message(INVALID_JSON_IN_CONFIGURATION_WARNING.format(filepath=filepath))
try:
# something is very wrong
with open(filepath, 'r') as file:
content = file.read()
return emitter.echo(content)
emitter.echo(content)
raise
except Exception:
# ... sorry.. we tried as hard as we could
raise # crash :-(
raise # crash :-(

View File

@ -24,7 +24,7 @@ from nucypher.blockchain.eth.token import NU
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.cli.literature import (
ABORT_DEPLOYMENT,
CONFIRM_ENABLE_RESTAKING,
CHARACTER_DESTRUCTION, CONFIRM_ENABLE_RESTAKING,
CONFIRM_ENABLE_WINDING_DOWN,
CONFIRM_LARGE_STAKE_DURATION,
CONFIRM_LARGE_STAKE_VALUE,
@ -34,6 +34,7 @@ from nucypher.cli.literature import (
RESTAKING_LOCK_AGREEMENT,
WINDING_DOWN_AGREEMENT
)
from nucypher.config.node import CharacterConfiguration
def confirm_deployment(emitter: StdoutEmitter, deployer_interface: BlockchainDeployerInterface) -> bool:
@ -90,3 +91,20 @@ def confirm_large_stake(value: NU = None, lock_periods: int = None) -> bool:
if lock_periods and (lock_periods > 365):
click.confirm(CONFIRM_LARGE_STAKE_DURATION.format(lock_periods=lock_periods), abort=True)
return True
def confirm_destroy_configuration(config: CharacterConfiguration) -> bool:
"""Interactively confirm destruction of nucypher configuration files"""
# TODO: This is a workaround for ursula - needs follow up
try:
database = config.db_filepath
except AttributeError:
database = "No database found"
confirmation = CHARACTER_DESTRUCTION.format(name=config.NAME,
root=config.config_root,
keystore=config.keyring_root,
nodestore=config.node_storage.source,
config=config.filepath,
database=database)
click.confirm(confirmation, abort=True)
return True

View File

@ -330,10 +330,12 @@ def config(general_config, config_file, full_config_options):
emitter = setup_emitter(general_config)
configuration_file_location = config_file or AliceConfiguration.default_filepath()
emitter.echo(f"Alice Configuration {configuration_file_location} \n {'='*55}")
update_configuration(emitter=emitter,
config_class=AliceConfiguration,
filepath=configuration_file_location,
config_options=full_config_options)
updates = full_config_options.get_updates()
if updates:
update_configuration(emitter=emitter,
config_class=AliceConfiguration,
filepath=configuration_file_location,
updates=updates)
@alice.command()

View File

@ -227,10 +227,12 @@ def config(general_config, config_options, config_file):
bob_config = config_options.create_config(emitter, config_file)
filepath = config_file or bob_config.config_file_location
emitter.echo(f"Bob Configuration {filepath} \n {'='*55}")
update_configuration(emitter=emitter,
config_class=BobConfiguration,
filepath=filepath,
config_options=config_options)
updates = config_options.get_updates()
if updates:
update_configuration(emitter=emitter,
config_class=BobConfiguration,
filepath=filepath,
updates=updates)
@bob.command()

View File

@ -305,10 +305,12 @@ def config(general_config, config_file, config_options):
emitter = setup_emitter(general_config)
configuration_file_location = config_file or StakeHolderConfiguration.default_filepath()
emitter.echo(f"StakeHolder Configuration {configuration_file_location} \n {'='*55}")
update_configuration(emitter=emitter,
config_class=StakeHolderConfiguration,
filepath=configuration_file_location,
config_options=config_options)
updates = config_options.get_updates()
if updates:
update_configuration(emitter=emitter,
config_class=StakeHolderConfiguration,
filepath=configuration_file_location,
updates=updates)
@stake.command('list')

View File

@ -253,7 +253,7 @@ class BaseConfiguration(ABC):
f"Expected version {cls.VERSION}; Got version {version}")
return deserialized_payload
def update(self, filepath: str = None, modifier: str = None, **updates):
def update(self, filepath: str = None, modifier: str = None, **updates) -> None:
for field, value in updates.items():
try:
getattr(self, field)

View File

@ -364,10 +364,7 @@ class CharacterConfiguration(BaseConfiguration):
"""Initialize a CharacterConfiguration from a JSON file."""
filepath = filepath or cls.default_filepath()
assembled_params = cls.assemble(filepath=filepath, **overrides)
try:
node_configuration = cls(filepath=filepath, provider_process=provider_process, **assembled_params)
except TypeError as e:
raise cls.ConfigurationError(e)
node_configuration = cls(filepath=filepath, provider_process=provider_process, **assembled_params)
return node_configuration
def validate(self) -> bool:

View File

@ -82,6 +82,12 @@ class NodeStorage(ABC):
def __iter__(self):
return self.all(federated_only=self.federated_only)
@property
@abstractmethod
def source(self) -> str:
"""Human readable source string"""
return NotImplemented
def _read_common_name(self, certificate: Certificate):
x509 = OpenSSL.crypto.X509.from_cryptography(certificate)
subject_components = x509.get_subject().get_components()
@ -195,9 +201,10 @@ class ForgetfulNodeStorage(NodeStorage):
self.__temporary_certificates = list()
self._temp_certificates_dir = tempfile.mkdtemp(prefix='nucypher-temp-certs-', dir=parent_dir)
# TODO: Pending fix for 1554.
# def __del__(self):
# shutil.rmtree(self._temp_certificates_dir, ignore_errors=True)
@property
def source(self) -> str:
"""Human readable source string"""
return self._name
def all(self, federated_only: bool, certificates_only: bool = False) -> set:
return set(self.__metadata.values() if not certificates_only else self.__certificates.values())
@ -384,6 +391,11 @@ class LocalFileBasedNodeStorage(NodeStorage):
self.certificates_dir = certificates_dir
self._cache_storage_filepaths(config_root=config_root)
@property
def source(self) -> str:
"""Human readable source string"""
return self.root_dir
@staticmethod
def _generate_storage_filepaths(config_root: str = None,
storage_root: str = None,

View File

@ -1,15 +1,29 @@
from pathlib import Path
import click
import pytest
from pathlib import Path
from nucypher.cli.actions.config import destroy_configuration, forget, handle_invalid_configuration_file, \
handle_missing_configuration_file, update_configuration
from nucypher.cli.literature import MISSING_CONFIGURATION_FILE
from nucypher.cli.actions.config import (
destroy_configuration,
forget,
handle_invalid_configuration_file,
handle_missing_configuration_file,
update_configuration
)
from nucypher.cli.literature import MISSING_CONFIGURATION_FILE, SUCCESSFUL_DESTRUCTION
from nucypher.config.characters import UrsulaConfiguration
from nucypher.config.keyring import NucypherKeyring
from nucypher.config.node import CharacterConfiguration
from tests.constants import YES
BAD_CONFIG_PAYLOADS = (
{'some': 'garbage'},
'some garbage',
2,
''
)
def test_forget(alice_blockchain_test_config,
test_emitter,
stdout_trap,
@ -20,48 +34,110 @@ def test_forget(alice_blockchain_test_config,
def test_update_configuration(alice_blockchain_test_config,
test_emitter,
stdout_trap):
stdout_trap,
mocker,
test_registry_source_manager):
# Setup
config_class = alice_blockchain_test_config.__class__
config_file = alice_blockchain_test_config.filepath
# Test Data
raw_payload = alice_blockchain_test_config.serialize()
JSON_payload = alice_blockchain_test_config.deserialize(payload=raw_payload)
# Isolate from filesystem and Spy on the methods we're testing here
mocker.patch('__main__.open', return_value=raw_payload)
mocker.patch.object(config_class, '_read_configuration_file', return_value=JSON_payload)
ghostwriter = mocker.patch.object(config_class, '_write_configuration_file', return_value=config_file)
spy_update = mocker.spy(config_class, 'update')
# Test
updates = dict(federated_only=True)
assert not alice_blockchain_test_config.federated_only
update_configuration(emitter=test_emitter,
config_class=config_class,
filepath=config_file,
config_options=dict())
updates=updates)
# The stand-in configuration is untouched...
assert not alice_blockchain_test_config.federated_only
# ... but updates were passed aloing to the config file system writing handlers
ghostwriter.assert_called_once_with(filepath=alice_blockchain_test_config.filepath, override=True)
assert spy_update.call_args.kwargs == updates
def test_destroy_configuration(alice_blockchain_test_config,
test_emitter,
stdout_trap):
destroy_configuration(emitter=test_emitter, character_config=alice_blockchain_test_config)
stdout_trap,
mocker,
mock_click_confirm):
def test_handle_missing_configuration_file(alice_blockchain_test_config):
# Setup
config = alice_blockchain_test_config
config_class = alice_blockchain_test_config.__class__
config_file = Path(alice_blockchain_test_config.filepath)
# The config file does not exist
assert not config_file.exists()
# Isolate from filesystem and Spy on the methods we're testing here
spy_keyring_attached = mocker.spy(CharacterConfiguration, 'attach_keyring')
spy_keyring_destroy = mocker.spy(NucypherKeyring, 'destroy')
mock_os_remove = mocker.patch('os.remove')
# Test
mock_click_confirm.return_value = YES
destroy_configuration(emitter=test_emitter, character_config=alice_blockchain_test_config)
output = stdout_trap.getvalue()
assert SUCCESSFUL_DESTRUCTION in output
spy_keyring_attached.assert_called_once()
spy_keyring_destroy.assert_called_once()
mock_os_remove.assert_called_with(str(config_file))
if config_class is UrsulaConfiguration:
mock_os_remove.assert_called_with(filepath=config.db_filepath)
def test_handle_missing_configuration_file(alice_blockchain_test_config):
# Setup
config_class = alice_blockchain_test_config.__class__
config_file = Path(alice_blockchain_test_config.filepath)
# Test Data
init_command = f"{config_class.NAME} init"
name = config_class.NAME.capitalize()
message = MISSING_CONFIGURATION_FILE.format(name=name, init_command=init_command)
# Context: The config file does not exist
assert not config_file.exists()
# Test
with pytest.raises(click.exceptions.FileError, match=message):
handle_missing_configuration_file(config_file=str(config_file),
character_config_class=config_class)
@pytest.mark.parametrize('bad_config_payload', BAD_CONFIG_PAYLOADS)
def test_handle_invalid_configuration_file(mocker,
alice_blockchain_test_config,
test_emitter,
stdout_trap):
stdout_trap,
bad_config_payload):
# Setup
config_class = alice_blockchain_test_config.__class__
config_file = alice_blockchain_test_config.filepath
config_file = Path(alice_blockchain_test_config.filepath)
# Assume the file exists but is full of garbage
mocker.patch.object(CharacterConfiguration,
'_read_configuration_file',
return_value={'some': 'garbage'})
return_value=bad_config_payload)
handle_invalid_configuration_file(emitter=test_emitter,
config_class=config_class,
filepath=config_file)
# Test
with pytest.raises(config_class.ConfigurationError):
handle_invalid_configuration_file(emitter=test_emitter,
config_class=config_class,
filepath=config_file)