mirror of https://github.com/nucypher/nucypher.git
CLI config action test coverage and related bug fixes.
parent
b9de67505d
commit
4a5aed06a1
|
@ -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 :-(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue