diff --git a/CHANGELOG.md b/CHANGELOG.md index 578c9f488..76a568f32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 The intended audience of this file is for py42 consumers -- as such, changes that don't affect how a consumer would use the library (e.g. adding unit tests, updating documentation, etc) are not captured here. +## 0.4.1 - 2020-03-13 + +### Fixed + +- Bug where `profile reset-pw` did not work with the default profile. +- Bug where `profile show` indicated a password was set for a different profile. +- We now validate credentials when setting a password. + + +### Changed + +- Date inputs are now required to be in quotes when they include a time. + ## 0.4.0 - 2020-03-12 ### Added diff --git a/src/code42cli/__version__.py b/src/code42cli/__version__.py index 6a9beea82..3d26edf77 100644 --- a/src/code42cli/__version__.py +++ b/src/code42cli/__version__.py @@ -1 +1 @@ -__version__ = "0.4.0" +__version__ = "0.4.1" diff --git a/src/code42cli/profile/password.py b/src/code42cli/profile/password.py index b08c85b0e..ec2b65c3f 100644 --- a/src/code42cli/profile/password.py +++ b/src/code42cli/profile/password.py @@ -9,30 +9,32 @@ _ROOT_SERVICE_NAME = u"code42cli" -def get_password(profile_name): - """Gets your currently stored password for your profile.""" - accessor = get_config_accessor() - profile = accessor.get_profile(profile_name) - service_name = _get_service_name(profile_name) +def get_stored_password(profile_name): + """Gets your currently stored password for the given profile name.""" + profile = _get_profile(profile_name) + service_name = _get_service_name(profile.name) username = _get_username(profile) password = keyring.get_password(service_name, username) return password -def set_password_from_prompt(profile_name): - """Prompts and sets your password for your profile.""" - password = getpass() - accessor = get_config_accessor() - profile = accessor.get_profile(profile_name) - service_name = _get_service_name(profile_name) +def get_password_from_prompt(): + """Prompts you and returns what you input.""" + return getpass() + + +def set_password(profile_name, new_password): + """Sets your password for the given profile name.""" + profile = _get_profile(profile_name) + service_name = _get_service_name(profile.name) username = _get_username(profile) - keyring.set_password(service_name, username, password) + keyring.set_password(service_name, username, new_password) print(u"'Code42 Password' updated.") - return password -def get_password_from_prompt(): - return getpass() +def _get_profile(profile_name): + accessor = get_config_accessor() + return accessor.get_profile(profile_name) def _get_service_name(profile_name): diff --git a/src/code42cli/profile/profile.py b/src/code42cli/profile/profile.py index 2c06d0082..0639c66e7 100644 --- a/src/code42cli/profile/profile.py +++ b/src/code42cli/profile/profile.py @@ -10,6 +10,7 @@ print_set_profile_help, print_no_existing_profile_message, ) +from code42cli.sdk_client import validate_connection class Code42Profile(object): @@ -33,7 +34,7 @@ def ignore_ssl_error(self): return self._profile[ConfigAccessor.IGNORE_SSL_ERRORS_KEY] def get_password(self): - pwd = password.get_password(self.name) + pwd = password.get_stored_password(self.name) if not pwd: pwd = password.get_password_from_prompt() return pwd @@ -91,7 +92,7 @@ def show_profile(args): print(u"\t* {0} = {1}".format(ConfigAccessor.USERNAME_KEY, profile.username)) print(u"\t* {0} = {1}".format(ConfigAccessor.AUTHORITY_KEY, profile.authority_url)) print(u"\t* {0} = {1}".format(ConfigAccessor.IGNORE_SSL_ERRORS_KEY, profile.ignore_ssl_error)) - if password.get_password(args.profile_name) is not None: + if password.get_stored_password(profile.name) is not None: print(u"\t* A password is set.") print(u"") @@ -109,7 +110,15 @@ def set_profile(args): def prompt_for_password_reset(args): """Securely prompts for your password and then stores it using keyring.""" - password.set_password_from_prompt(args.profile_name) + profile = get_profile(args.profile_name) + new_password = password.get_password_from_prompt() + if not validate_connection(profile.authority_url, profile.username, new_password): + print_error( + "Your password was not saved because your credentials failed to validate. " + "Check your network connection and the spelling of your username and server URL." + ) + exit(1) + password.set_password(profile.name, new_password) def list_profiles(*args): @@ -130,7 +139,7 @@ def use_profile(args): try: accessor.switch_default_profile(args.profile_name) except Exception as ex: - print_error(ex) + print_error(str(ex)) exit(1) @@ -198,9 +207,10 @@ def _verify_args_for_set(args): missing_values = not args.c42_username and not args.c42_authority_url if missing_values: try: - profile = get_profile(args.profile_name) + accessor = get_config_accessor() + profile = Code42Profile(accessor.get_profile(args.profile_name)) missing_values = not profile.username and not profile.authority_url - except SystemExit: + except Exception: missing_values = True if missing_values: @@ -231,10 +241,10 @@ def _missing_default_profile(args): profile_name_arg_is_none = ( args.profile_name is None or args.profile_name == ConfigAccessor.DEFAULT_VALUE ) - return profile_name_arg_is_none and not _default_profile_exists() + return profile_name_arg_is_none and not _default_profile_exist() -def _default_profile_exists(): +def _default_profile_exist(): try: accessor = get_config_accessor() profile = Code42Profile(accessor.get_profile()) diff --git a/src/code42cli/sdk_client.py b/src/code42cli/sdk_client.py new file mode 100644 index 000000000..875edfc66 --- /dev/null +++ b/src/code42cli/sdk_client.py @@ -0,0 +1,28 @@ +from py42 import debug_level +from py42 import settings +from py42.sdk import SDK + +from code42cli.util import print_error + + +def create_sdk(profile, is_debug_mode): + if is_debug_mode: + settings.debug_level = debug_level.DEBUG + try: + password = profile.get_password() + return SDK.create_using_local_account(profile.authority_url, profile.username, password) + except Exception: + print_error( + u"Invalid credentials or host address. " + u"Verify your profile is set up correctly and that you are supplying the correct password." + ) + exit(1) + + +def validate_connection(authority_url, username, password): + try: + SDK.create_using_local_account(authority_url, username, password) + return True + except: + print(username, password, authority_url) + return False diff --git a/src/code42cli/securitydata/arguments/search.py b/src/code42cli/securitydata/arguments/search.py index b10c2a071..11523566e 100644 --- a/src/code42cli/securitydata/arguments/search.py +++ b/src/code42cli/securitydata/arguments/search.py @@ -70,7 +70,6 @@ def _add_begin_date_arg(parser): parser.add_argument( u"-b", u"--begin", - nargs=u"+", action=u"store", dest=SearchArguments.BEGIN_DATE, help=u"The beginning of the date range in which to look for events, " @@ -82,7 +81,6 @@ def _add_end_date_arg(parser): parser.add_argument( u"-e", u"--end", - nargs=u"+", action=u"store", dest=SearchArguments.END_DATE, help=u"The end of the date range in which to look for events, " diff --git a/src/code42cli/securitydata/date_helper.py b/src/code42cli/securitydata/date_helper.py index 07fd85900..6a0581a36 100644 --- a/src/code42cli/securitydata/date_helper.py +++ b/src/code42cli/securitydata/date_helper.py @@ -63,24 +63,24 @@ def _verify_timestamp_order(min_timestamp, max_timestamp): raise ValueError(u"Begin date cannot be after end date") -def _parse_timestamp(date_tuple): +def _parse_timestamp(date_and_time): try: - date_str = _join_date_tuple(date_tuple) - date_format = u"%Y-%m-%d" if len(date_tuple) == 1 else u"%Y-%m-%d %H:%M:%S" + date_str = _join_date_and_time(date_and_time) + date_format = u"%Y-%m-%d" if len(date_and_time) == 1 else u"%Y-%m-%d %H:%M:%S" time = datetime.strptime(date_str, date_format) except ValueError: raise ValueError(_FORMAT_VALUE_ERROR_MESSAGE) return convert_datetime_to_timestamp(time) -def _join_date_tuple(date_tuple): - if not date_tuple: +def _join_date_and_time(date_and_time): + if not date_and_time: return None - date_str = date_tuple[0] - if len(date_tuple) == 1: + date_str = date_and_time[0] + if len(date_and_time) == 1: return date_str - if len(date_tuple) == 2: - date_str = "{0} {1}".format(date_str, date_tuple[1]) + if len(date_and_time) == 2: + date_str = "{0} {1}".format(date_str, date_and_time[1]) else: raise ValueError(_FORMAT_VALUE_ERROR_MESSAGE) return date_str diff --git a/src/code42cli/securitydata/extraction.py b/src/code42cli/securitydata/extraction.py index ed2a95d06..203269801 100644 --- a/src/code42cli/securitydata/extraction.py +++ b/src/code42cli/securitydata/extraction.py @@ -4,9 +4,6 @@ from c42eventextractor import FileEventHandlers from c42eventextractor.extractors import FileEventExtractor -from py42 import debug_level -from py42 import settings -from py42.sdk import SDK from py42.sdk.file_event_query.cloud_query import Actor from py42.sdk.file_event_query.device_query import DeviceUsername from py42.sdk.file_event_query.event_query import Source @@ -22,6 +19,7 @@ from code42cli.securitydata.logger_factory import get_error_logger from code42cli.securitydata.options import ExposureType as ExposureTypeOptions from code42cli.util import print_error, print_bold, is_interactive +from code42cli.sdk_client import create_sdk _EXCEPTIONS_OCCURRED = False @@ -41,7 +39,7 @@ def extract(output_logger, args): store = _create_cursor_store(args, profile) filters = _get_filters(args, store) handlers = _create_event_handlers(output_logger, store) - sdk = _get_sdk(profile, args.is_debug_mode) + sdk = create_sdk(profile, args.is_debug_mode) extractor = FileEventExtractor(sdk, handlers) _call_extract(extractor, filters, args) _handle_result() @@ -85,12 +83,12 @@ def _verify_begin_date_requirements(args, cursor_store): def _begin_date_is_required(args, cursor_store): if not args.is_incremental: return True - required = cursor_store is not None and cursor_store.get_stored_insertion_timestamp() is None + is_required = cursor_store and cursor_store.get_stored_insertion_timestamp() is None # Ignore begin date when is incremental mode, it is not required, and it was passed an argument. - if not required and args.begin_date: + if not is_required and args.begin_date: args.begin_date = None - return required + return is_required def _verify_exposure_types(exposure_types): @@ -122,7 +120,9 @@ def _create_filters(args): def _get_event_timestamp_filter(args): try: - return date_helper.create_event_timestamp_filter(args.begin_date, args.end_date) + begin_date = args.begin_date.strip().split(" ") if args.begin_date else None + end_date = args.end_date.strip().split(" ") if args.end_date else None + return date_helper.create_event_timestamp_filter(begin_date, end_date) except ValueError as ex: print_error(str(ex)) exit(1) @@ -153,20 +153,6 @@ def handle_response(response): return handlers -def _get_sdk(profile, is_debug_mode): - if is_debug_mode: - settings.debug_level = debug_level.DEBUG - try: - password = profile.get_password() - return SDK.create_using_local_account(profile.authority_url, profile.username, password) - except Exception: - print_error( - u"Invalid credentials or host address. " - u"Verify your profile is set up correctly and that you are supplying the correct password." - ) - exit(1) - - def _call_extract(extractor, filters, args): if args.advanced_query: extractor.extract_advanced(args.advanced_query) diff --git a/src/code42cli/securitydata/logger_factory.py b/src/code42cli/securitydata/logger_factory.py index 3bcad6228..e1c91fea1 100644 --- a/src/code42cli/securitydata/logger_factory.py +++ b/src/code42cli/securitydata/logger_factory.py @@ -47,7 +47,7 @@ def get_logger_for_file(filename, output_format): with _logger_deps_lock: if not _logger_has_handlers(logger): - handler = logging.FileHandler(filename, delay=True) + handler = logging.FileHandler(filename, delay=True, encoding="utf-8") return _init_logger(logger, handler, output_format) return logger @@ -86,7 +86,7 @@ def get_error_logger(): with _logger_deps_lock: if not _logger_has_handlers(logger): formatter = logging.Formatter(u"%(asctime)s %(message)s") - handler = RotatingFileHandler(log_path, maxBytes=250000000) + handler = RotatingFileHandler(log_path, maxBytes=250000000, encoding="utf-8") return _apply_logger_dependencies(logger, handler, formatter) return logger diff --git a/src/code42cli/util.py b/src/code42cli/util.py index ae107ea91..81fb48136 100644 --- a/src/code42cli/util.py +++ b/src/code42cli/util.py @@ -27,13 +27,13 @@ def get_user_project_path(subdir=""): def open_file(file_path, mode, action): """Wrapper for opening files, useful for testing purposes.""" - with open(file_path, mode) as f: + with open(file_path, mode, encoding="utf-8") as f: action(f) def print_error(error_text): """Prints red text.""" - print("\033[91mUSAGE ERROR: {}\033[0m".format(error_text)) + print("\033[91mERROR: {}\033[0m".format(error_text)) def print_bold(bold_text): diff --git a/tests/conftest.py b/tests/conftest.py index 7c3df3e6b..d153fab27 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,9 @@ import pytest +from code42cli.profile.config import ConfigAccessor +from code42cli.profile.profile import Code42Profile + @pytest.fixture def namespace(mocker): @@ -28,6 +31,46 @@ def namespace(mocker): return mock +def create_profile_values_dict(authority=None, username=None, ignore_ssl=False): + return { + ConfigAccessor.AUTHORITY_KEY: authority, + ConfigAccessor.USERNAME_KEY: username, + ConfigAccessor.IGNORE_SSL_ERRORS_KEY: ignore_ssl, + } + + +class MockSection(object): + def __init__(self, name="Test Profile Name", values_dict=None): + self.name = name + self.values_dict = values_dict or create_profile_values_dict() + + def __getitem__(self, item): + return self.values_dict[item] + + def __setitem__(self, key, value): + self.values_dict[key] = value + + def get(self, item): + return self.values_dict.get(item) + + +def create_mock_profile(name=None): + profile_section = MockSection(name) + profile = Code42Profile(profile_section) + + def mock_get_password(): + return "Test Password" + + profile.get_password = mock_get_password + return profile + + +def setup_mock_accessor(mock_accessor, name=None, values_dict=None): + profile_section = MockSection(name, values_dict) + mock_accessor.get_profile.return_value = profile_section + return mock_accessor + + def get_filter_value_from_json(json, filter_index): return json_module.loads(str(json))["filters"][filter_index]["value"] diff --git a/tests/profile/test_config.py b/tests/profile/test_config.py index f895c9515..b9cfe688d 100644 --- a/tests/profile/test_config.py +++ b/tests/profile/test_config.py @@ -1,10 +1,10 @@ from __future__ import with_statement -from configparser import ConfigParser - import pytest +from configparser import ConfigParser from code42cli.profile.config import ConfigAccessor +from ..conftest import MockSection @pytest.fixture @@ -17,26 +17,11 @@ def mock_saver(mocker): return mocker.patch("code42cli.util.open_file") -def create_mock_profile_object(name, authority=None, username=None): - authority = authority or ConfigAccessor.DEFAULT_VALUE - username = username or ConfigAccessor.DEFAULT_VALUE - profile_dict = {ConfigAccessor.AUTHORITY_KEY: authority, ConfigAccessor.USERNAME_KEY: username} - - class ProfileObject(object): - def __getitem__(self, item): - return profile_dict[item] - - def __setitem__(self, key, value): - profile_dict[key] = value - - def get(self, item): - return profile_dict.get(item) - - @property - def name(self): - return name - - return ProfileObject() +def create_mock_profile_object(profile_name, authority_url=None, username=None): + mock_profile = MockSection(profile_name) + mock_profile[ConfigAccessor.AUTHORITY_KEY] = authority_url + mock_profile[ConfigAccessor.USERNAME_KEY] = username + return mock_profile def create_internal_object(is_complete, default_profile_name=None): @@ -45,18 +30,13 @@ def create_internal_object(is_complete, default_profile_name=None): ConfigAccessor.DEFAULT_PROFILE: default_profile_name, ConfigAccessor.DEFAULT_PROFILE_IS_COMPLETE: is_complete, } + internal_section = MockSection("Internal", internal_dict) - class InternalObject(object): - def __getitem__(self, item): - return internal_dict[item] - - def __setitem__(self, key, value): - internal_dict[key] = value + def getboolean(*args): + return is_complete - def getboolean(self, *args): - return is_complete - - return InternalObject() + internal_section.getboolean = getboolean + return internal_section def setup_parser_one_profile(profile, internal, parser): @@ -99,7 +79,7 @@ def test_get_all_profiles_excludes_internal_section(self, mock_config_parser): def test_set_username_marks_as_complete_if_ready(self, mock_config_parser): mock_config_parser.sections.return_value = ["Internal", "ProfileA"] accessor = ConfigAccessor(mock_config_parser) - mock_profile = create_mock_profile_object("ProfileA", "example.com", None) + mock_profile = create_mock_profile_object("ProfileA", "www.example.com", None) mock_internal = create_internal_object(False) setup_parser_one_profile(mock_profile, mock_internal, mock_config_parser) accessor.set_username("TestUser", "ProfileA") @@ -119,7 +99,7 @@ def test_set_username_does_not_mark_as_complete_if_not_have_authority(self, mock def test_set_username_saves(self, mock_config_parser, mock_saver): mock_config_parser.sections.return_value = ["Internal", "ProfileA"] accessor = ConfigAccessor(mock_config_parser) - mock_profile = create_mock_profile_object("ProfileA", "example.com", "console.com") + mock_profile = create_mock_profile_object("ProfileA", "www.example.com", "username") mock_internal = create_internal_object(True, "ProfileA") setup_parser_one_profile(mock_profile, mock_internal, mock_config_parser) accessor.set_username("TestUser", "ProfileA") @@ -154,6 +134,27 @@ def test_set_authority_saves(self, mock_config_parser, mock_saver): accessor.set_authority_url("new url", "ProfileA") assert mock_saver.call_count + def test_get_all_profiles_returns_profiles_with_expected_values(self, mock_config_parser): + mock_config_parser.sections.return_value = ["Internal", "ProfileA", "ProfileB"] + accessor = ConfigAccessor(mock_config_parser) + mock_profile_a = create_mock_profile_object("ProfileA", "test", "test") + mock_profile_b = create_mock_profile_object("ProfileB", "test", "test") + + mock_internal = create_internal_object(True, "ProfileA") + + def side_effect(item): + if item == "ProfileA": + return mock_profile_a + elif item == "ProfileB": + return mock_profile_b + elif item == "Internal": + return mock_internal + + mock_config_parser.__getitem__.side_effect = side_effect + profiles = accessor.get_all_profiles() + assert profiles[0].name == "ProfileA" + assert profiles[1].name == "ProfileB" + def test_switch_default_profile_switches_internal_value(self, mock_config_parser): mock_config_parser.sections.return_value = ["Internal", "ProfileA", "ProfileB"] accessor = ConfigAccessor(mock_config_parser) diff --git a/tests/profile/test_password.py b/tests/profile/test_password.py index 2665f07fd..a44de41a9 100644 --- a/tests/profile/test_password.py +++ b/tests/profile/test_password.py @@ -3,6 +3,7 @@ import code42cli.profile.password as password from code42cli.profile.config import ConfigAccessor from .conftest import PASSWORD_NAMESPACE +from ..conftest import setup_mock_accessor, create_profile_values_dict _USERNAME = "test.username" @@ -12,12 +13,6 @@ def config_accessor(mocker): mock = mocker.MagicMock(spec=ConfigAccessor) factory = mocker.patch("{0}.get_config_accessor".format(PASSWORD_NAMESPACE)) factory.return_value = mock - - class MockConfigProfile(object): - def __getitem__(self, item): - return _USERNAME - - mock.get_profile.return_value = MockConfigProfile() return mock @@ -36,28 +31,42 @@ def getpass_function(mocker): return mocker.patch("code42cli.profile.password.getpass") -def test_get_password_uses_expected_service_name_and_username( +def test_get_stored_password_when_given_profile_name_gets_profile_for_that_name( keyring_password_getter, config_accessor ): - password.get_password("profile_name") - expected_service_name = "code42cli::profile_name" - keyring_password_getter.assert_called_once_with(expected_service_name, _USERNAME) + password.get_stored_password("profile_name") + config_accessor.get_profile.assert_called_once_with("profile_name") -def test_get_password_returns_expected_password( +def test_get_stored_password_returns_expected_password( keyring_password_getter, config_accessor, keyring_password_setter ): keyring_password_getter.return_value = "already stored password 123" - assert password.get_password("profile_name") == "already stored password 123" + assert password.get_stored_password("profile_name") == "already stored password 123" -def test_set_password_from_prompt_uses_expected_service_name_username_and_password( - keyring_password_setter, config_accessor, getpass_function +def test_set_password_uses_expected_service_name_username_and_password( + keyring_password_setter, config_accessor ): - getpass_function.return_value = "test password" - password.set_password_from_prompt("profile_name") + values = create_profile_values_dict(username="test.username") + setup_mock_accessor(config_accessor, "profile_name", values) + password.set_password("profile_name", "test_password") expected_service_name = "code42cli::profile_name" expected_username = "test.username" keyring_password_setter.assert_called_once_with( - expected_service_name, expected_username, "test password" + expected_service_name, expected_username, "test_password" + ) + + +def test_set_password_when_given_none_uses_password_from_default_profile( + keyring_password_setter, config_accessor +): + values = create_profile_values_dict(username="test.username") + setup_mock_accessor(config_accessor, "Default_Profile", values) + config_accessor.name = "Default_Profile" + password.set_password(None, "test_password") + expected_service_name = "code42cli::Default_Profile" + expected_username = "test.username" + keyring_password_setter.assert_called_once_with( + expected_service_name, expected_username, "test_password" ) diff --git a/tests/profile/test_profile.py b/tests/profile/test_profile.py index 563f9fe3d..75bd4cd73 100644 --- a/tests/profile/test_profile.py +++ b/tests/profile/test_profile.py @@ -5,11 +5,12 @@ from code42cli.profile import profile from code42cli.profile.config import ConfigAccessor from .conftest import PASSWORD_NAMESPACE, PROFILE_NAMESPACE +from ..conftest import MockSection, create_mock_profile @pytest.fixture def config_accessor(mocker): - mock = mocker.MagicMock(spec=ConfigAccessor) + mock = mocker.MagicMock(spec=ConfigAccessor, name="Config Accessor") factory = mocker.patch("{0}.profile.get_config_accessor".format(PROFILE_NAMESPACE)) factory.return_value = mock return mock @@ -17,12 +18,12 @@ def config_accessor(mocker): @pytest.fixture(autouse=True) def password_setter(mocker): - return mocker.patch("{0}.set_password_from_prompt".format(PASSWORD_NAMESPACE)) + return mocker.patch("{0}.set_password".format(PASSWORD_NAMESPACE)) @pytest.fixture(autouse=True) def password_getter(mocker): - return mocker.patch("{0}.get_password".format(PASSWORD_NAMESPACE)) + return mocker.patch("{0}.get_stored_password".format(PASSWORD_NAMESPACE)) @pytest.fixture(autouse=True) @@ -30,33 +31,23 @@ def input_function(mocker): return mocker.patch("{0}.profile.get_input".format(PROFILE_NAMESPACE)) -def _get_profile_parser(): +def _get_arg_parser(): subcommand_parser = ArgumentParser().add_subparsers() profile.init(subcommand_parser) return subcommand_parser.choices.get("profile") -def create_profile(): - class MockSection(object): - name = "TEST" - - def get(*args): - pass - - return profile.Code42Profile(MockSection()) - - class TestCode42Profile(object): def test_get_password_when_is_none_returns_password_from_getpass(self, mocker, password_getter): password_getter.return_value = None mock_getpass = mocker.patch("{0}.get_password_from_prompt".format(PASSWORD_NAMESPACE)) mock_getpass.return_value = "Test Password" - actual = create_profile().get_password() + actual = create_mock_profile().get_password() assert actual == "Test Password" def test_get_password_return_password_from_password_get_password(self, password_getter): password_getter.return_value = "Test Password" - actual = create_profile().get_password() + actual = create_mock_profile().get_password() assert actual == "Test Password" @@ -66,14 +57,21 @@ def test_init_adds_profile_subcommand_to_choices(config_accessor): assert subcommand_parser.choices.get("profile") -def test_init_adds_parser_that_can_parse_show_command(config_accessor): +def test_init_adds_parser_that_can_parse_show_command_without_profile(config_accessor): + subcommand_parser = ArgumentParser().add_subparsers() + profile.init(subcommand_parser) + profile_parser = subcommand_parser.choices.get("profile") + assert profile_parser.parse_args(["show"]) + + +def test_init_adds_parser_that_can_parse_show_command_with_profile(config_accessor): subcommand_parser = ArgumentParser().add_subparsers() profile.init(subcommand_parser) profile_parser = subcommand_parser.choices.get("profile") assert profile_parser.parse_args(["show", "--profile", "name"]) -def test_init_adds_parser_that_can_parse_set_command(config_accessor): +def test_init_adds_parser_that_can_parse_set_command_without_profile(config_accessor): subcommand_parser = ArgumentParser().add_subparsers() profile.init(subcommand_parser) profile_parser = subcommand_parser.choices.get("profile") @@ -82,6 +80,24 @@ def test_init_adds_parser_that_can_parse_set_command(config_accessor): ) +def test_init_adds_parser_that_can_parse_set_command_with_profile(config_accessor): + subcommand_parser = ArgumentParser().add_subparsers() + profile.init(subcommand_parser) + profile_parser = subcommand_parser.choices.get("profile") + profile_parser.parse_args( + [ + "set", + "--profile", + "ProfileName", + "-s", + "server-arg", + "-u", + "username-arg", + "--enable-ssl-errors", + ] + ) + + def test_init_add_parser_that_can_parse_list_command(): subcommand_parser = ArgumentParser().add_subparsers() profile.init(subcommand_parser) @@ -104,14 +120,14 @@ def test_get_profile_returns_object_from_config_profile(mocker, config_accessor) def test_set_profile_when_given_username_sets_username(config_accessor): - parser = _get_profile_parser() + parser = _get_arg_parser() namespace = parser.parse_args(["set", "-u", "a.new.user@example.com"]) profile.set_profile(namespace) assert config_accessor.set_username.call_args[0][0] == "a.new.user@example.com" def test_set_profile_when_given_profile_name_sets_username_for_profile(config_accessor): - parser = _get_profile_parser() + parser = _get_arg_parser() namespace = parser.parse_args(["set", "--profile", "profileA", "-u", "a.new.user@example.com"]) profile.set_profile(namespace) assert config_accessor.set_username.call_args[0][0] == "a.new.user@example.com" @@ -119,29 +135,28 @@ def test_set_profile_when_given_profile_name_sets_username_for_profile(config_ac def test_set_profile_when_given_authority_sets_authority(config_accessor): - parser = _get_profile_parser() + parser = _get_arg_parser() namespace = parser.parse_args(["set", "-s", "example.com"]) profile.set_profile(namespace) assert config_accessor.set_authority_url.call_args[0][0] == "example.com" def test_set_profile_when_given_profile_name_sets_authority_for_profile(config_accessor): - parser = _get_profile_parser() + parser = _get_arg_parser() namespace = parser.parse_args(["set", "--profile", "profileA", "-s", "example.com"]) profile.set_profile(namespace) - assert config_accessor.set_authority_url.call_args[0][0] == "example.com" - assert config_accessor.set_authority_url.call_args[0][1] == "profileA" + assert config_accessor.set_authority_url.call_args[0] == ("example.com", "profileA") def test_set_profile_when_given_enable_ssl_errors_sets_ignore_ssl_errors_to_true(config_accessor): - parser = _get_profile_parser() + parser = _get_arg_parser() namespace = parser.parse_args(["set", "--enable-ssl-errors"]) profile.set_profile(namespace) assert config_accessor.set_ignore_ssl_errors.call_args[0][0] == False def test_set_profile_when_given_disable_ssl_errors_sets_ignore_ssl_errors_to_true(config_accessor): - parser = _get_profile_parser() + parser = _get_arg_parser() namespace = parser.parse_args(["set", "--disable-ssl-errors"]) profile.set_profile(namespace) assert config_accessor.set_ignore_ssl_errors.call_args[0][0] == True @@ -150,19 +165,21 @@ def test_set_profile_when_given_disable_ssl_errors_sets_ignore_ssl_errors_to_tru def test_set_profile_when_given_disable_ssl_errors_and_profile_name_sets_ignore_ssl_errors_to_true_for_profile( config_accessor ): - parser = _get_profile_parser() + parser = _get_arg_parser() namespace = parser.parse_args(["set", "--profile", "profileA", "--disable-ssl-errors"]) profile.set_profile(namespace) - assert config_accessor.set_ignore_ssl_errors.call_args[0][0] == True - assert config_accessor.set_ignore_ssl_errors.call_args[0][1] == "profileA" + assert config_accessor.set_ignore_ssl_errors.call_args[0] == (True, "profileA") def test_set_profile_when_to_store_password_prompts_for_storing_password( mocker, config_accessor, input_function ): + mock_successful_connection = mocker.patch("code42cli.profile.profile.validate_connection") + mock_successful_connection.return_value = True input_function.return_value = "y" - mock_set_password_function = mocker.patch("code42cli.profile.password.set_password_from_prompt") - parser = _get_profile_parser() + mocker.patch("code42cli.profile.password.get_password_from_prompt") + mock_set_password_function = mocker.patch("code42cli.profile.password.set_password") + parser = _get_arg_parser() namespace = parser.parse_args( ["set", "-s", "https://wwww.new.authority.example.com", "-u", "user"] ) @@ -170,34 +187,96 @@ def test_set_profile_when_to_store_password_prompts_for_storing_password( assert mock_set_password_function.call_count -def test_set_profile_when_told_to_store_password_using_capital_y_prompts_for_storing_password( +def test_set_profile_when_told_not_to_store_password_does_not_prompt_for_storing_password( mocker, config_accessor, input_function ): - input_function.return_value = "Y" - mock_set_password_function = mocker.patch("code42cli.profile.password.set_password_from_prompt") - parser = _get_profile_parser() + input_function.return_value = "n" + mocker.patch("code42cli.profile.password.get_password_from_prompt") + parser = _get_arg_parser() + mock_set_password_function = mocker.patch("code42cli.profile.password.set_password") namespace = parser.parse_args( ["set", "-s", "https://wwww.new.authority.example.com", "-u", "user"] ) profile.set_profile(namespace) - assert mock_set_password_function.call_count + assert not mock_set_password_function.call_count -def test_set_profile_when_told_not_to_store_password_prompts_for_storing_password( +def test_set_profile_when_told_to_store_password_but_connection_fails_exits( mocker, config_accessor, input_function ): - input_function.return_value = "n" - mock_set_password_function = mocker.patch("code42cli.profile.password.set_password_from_prompt") - parser = _get_profile_parser() + mock_successful_connection = mocker.patch("code42cli.profile.profile.validate_connection") + mock_successful_connection.return_value = False + input_function.return_value = "y" + mocker.patch("code42cli.profile.password.get_password_from_prompt") + parser = _get_arg_parser() namespace = parser.parse_args( ["set", "-s", "https://wwww.new.authority.example.com", "-u", "user"] ) - profile.set_profile(namespace) - assert not mock_set_password_function.call_count + with pytest.raises(SystemExit): + profile.set_profile(namespace) -def test_prompt_for_password_reset_calls_password_set_password_from_prompt(mocker, namespace): - namespace.profile_name = "profile name" - mock_set_password_function = mocker.patch("code42cli.profile.password.set_password_from_prompt") +def test_prompt_for_password_reset_when_connection_fails_does_not_reset_password( + mocker, config_accessor, input_function +): + mock_successful_connection = mocker.patch("code42cli.profile.profile.validate_connection") + mock_successful_connection.return_value = False + input_function.return_value = "y" + mocker.patch("code42cli.profile.password.get_password_from_prompt") + parser = _get_arg_parser() + namespace = parser.parse_args(["reset-pw", "--profile", "Test"]) + with pytest.raises(SystemExit): + profile.prompt_for_password_reset(namespace) + + +def test_prompt_for_password_when_not_given_profile_name_calls_set_password_with_default_profile( + mocker, config_accessor, input_function +): + default_profile = MockSection() + config_accessor.get_profile.return_value = default_profile + mock_successful_connection = mocker.patch("code42cli.profile.profile.validate_connection") + mock_successful_connection.return_value = True + input_function.return_value = "y" + password_prompt = mocker.patch("code42cli.profile.password.get_password_from_prompt") + password_prompt.return_value = "new password" + parser = _get_arg_parser() + namespace = parser.parse_args(["reset-pw"]) + mock_set_password_function = mocker.patch("code42cli.profile.password.set_password") profile.prompt_for_password_reset(namespace) - assert mock_set_password_function.call_count + mock_set_password_function.assert_called_once_with(default_profile.name, "new password") + + +def test_list_profiles_when_no_profiles_prints_error(mocker, config_accessor): + config_accessor.get_all_profiles.return_value = [] + mock_error_printer = mocker.patch("code42cli.util.print_error") + parser = _get_arg_parser() + namespace = parser.parse_args(["list"]) + profile.list_profiles(namespace) + mock_error_printer.assert_called_once_with("No existing profile.") + + +def test_list_profiles_when_profiles_exists_does_not_print_error(mocker, config_accessor): + config_accessor.get_all_profiles.return_value = [MockSection()] + mock_error_printer = mocker.patch("code42cli.util.print_error") + parser = _get_arg_parser() + namespace = parser.parse_args(["list"]) + profile.list_profiles(namespace) + assert not mock_error_printer.call_count + + +def test_use_profile_when_switching_fails_causes_exit(config_accessor): + def side_effect(*args): + raise Exception() + + config_accessor.switch_default_profile.side_effect = side_effect + parser = _get_arg_parser() + namespace = parser.parse_args(["use", "TestProfile"]) + with pytest.raises(SystemExit): + profile.use_profile(namespace) + + +def test_use_profile_calls_accessor_with_expected_profile_name(config_accessor): + parser = _get_arg_parser() + namespace = parser.parse_args(["use", "TestProfile"]) + profile.use_profile(namespace) + config_accessor.switch_default_profile.assert_called_once_with("TestProfile") diff --git a/tests/securitydata/conftest.py b/tests/securitydata/conftest.py index 28b64ff0d..1e2f6256d 100644 --- a/tests/securitydata/conftest.py +++ b/tests/securitydata/conftest.py @@ -5,11 +5,14 @@ SECURITYDATA_NAMESPACE = "code42cli.securitydata" SUBCOMMANDS_NAMESPACE = "{0}.subcommands".format(SECURITYDATA_NAMESPACE) - -begin_date_tuple = (get_test_date_str(days_ago=89),) -begin_date_tuple_with_time = (get_test_date_str(days_ago=89), "3:12:33") -end_date_tuple = (get_test_date_str(days_ago=10),) -end_date_tuple_with_time = (get_test_date_str(days_ago=10), "11:22:43") +begin_date_str = get_test_date_str(days_ago=89) +begin_date_str_with_time = "{0} 3:12:33".format(begin_date_str) +end_date_str = get_test_date_str(days_ago=10) +end_date_str_with_time = "{0} 11:22:43".format(end_date_str) +begin_date_list = [get_test_date_str(days_ago=89)] +begin_date_list_with_time = [get_test_date_str(days_ago=89), "3:12:33"] +end_date_list = [get_test_date_str(days_ago=10)] +end_date_list_with_time = [get_test_date_str(days_ago=10), "11:22:43"] @pytest.fixture(autouse=True) diff --git a/tests/securitydata/subcommands/test_print_out.py b/tests/securitydata/subcommands/test_print_out.py index d99ce361b..21a81281b 100644 --- a/tests/securitydata/subcommands/test_print_out.py +++ b/tests/securitydata/subcommands/test_print_out.py @@ -3,21 +3,12 @@ import pytest import code42cli.securitydata.subcommands.print_out as printer -from code42cli.profile.config import ConfigAccessor from .conftest import ACCEPTABLE_ARGS from ..conftest import SUBCOMMANDS_NAMESPACE _PRINT_PATH = "{0}.print_out".format(SUBCOMMANDS_NAMESPACE) -@pytest.fixture -def config_accessor(mocker): - mock = mocker.MagicMock(spec=ConfigAccessor) - factory = mocker.patch("") - factory.return_value = mock - return mock - - @pytest.fixture def logger_factory(mocker): return mocker.patch("{0}.get_logger_for_stdout".format(_PRINT_PATH)) diff --git a/tests/securitydata/test_date_helper.py b/tests/securitydata/test_date_helper.py index f57e8b44c..1094c2779 100644 --- a/tests/securitydata/test_date_helper.py +++ b/tests/securitydata/test_date_helper.py @@ -2,48 +2,58 @@ from code42cli.securitydata.date_helper import create_event_timestamp_filter from .conftest import ( - begin_date_tuple, - begin_date_tuple_with_time, - end_date_tuple, - end_date_tuple_with_time, + begin_date_list, + begin_date_list_with_time, + end_date_list, + end_date_list_with_time, ) from ..conftest import get_filter_value_from_json, get_test_date_str +def test_create_event_timestamp_filter_when_given_nothing_returns_none(): + ts_range = create_event_timestamp_filter() + assert not ts_range + + +def test_create_event_timestamp_filter_when_given_nones_returns_none(): + ts_range = create_event_timestamp_filter(None, None) + assert not ts_range + + def test_create_event_timestamp_filter_builds_expected_query(): - ts_range = create_event_timestamp_filter(begin_date_tuple) + ts_range = create_event_timestamp_filter(begin_date_list) actual = get_filter_value_from_json(ts_range, filter_index=0) - expected = "{0}T00:00:00.000Z".format(begin_date_tuple[0]) + expected = "{0}T00:00:00.000Z".format(begin_date_list[0]) assert actual == expected def test_create_event_timestamp_filter_when_given_begin_with_time_builds_expected_query(): - ts_range = create_event_timestamp_filter(begin_date_tuple_with_time) + ts_range = create_event_timestamp_filter(begin_date_list_with_time) actual = get_filter_value_from_json(ts_range, filter_index=0) - expected = "{0}T0{1}.000Z".format(begin_date_tuple_with_time[0], begin_date_tuple_with_time[1]) + expected = "{0}T0{1}.000Z".format(begin_date_list_with_time[0], begin_date_list_with_time[1]) assert actual == expected def test_create_event_timestamp_filter_when_given_end_builds_expected_query(): - ts_range = create_event_timestamp_filter(begin_date_tuple, end_date_tuple) + ts_range = create_event_timestamp_filter(begin_date_list, end_date_list) actual = get_filter_value_from_json(ts_range, filter_index=1) - expected = "{0}T23:59:59.000Z".format(end_date_tuple[0]) + expected = "{0}T23:59:59.000Z".format(end_date_list[0]) assert actual == expected def test_create_event_timestamp_filter_when_given_end_with_time_builds_expected_query(): - ts_range = create_event_timestamp_filter(begin_date_tuple, end_date_tuple_with_time) + ts_range = create_event_timestamp_filter(begin_date_list, end_date_list_with_time) actual = get_filter_value_from_json(ts_range, filter_index=1) - expected = "{0}T{1}.000Z".format(end_date_tuple_with_time[0], end_date_tuple_with_time[1]) + expected = "{0}T{1}.000Z".format(end_date_list_with_time[0], end_date_list_with_time[1]) assert actual == expected def test_create_event_timestamp_filter_when_given_both_begin_and_end_builds_expected_query(): - ts_range = create_event_timestamp_filter(begin_date_tuple, end_date_tuple_with_time) + ts_range = create_event_timestamp_filter(begin_date_list, end_date_list_with_time) actual_begin = get_filter_value_from_json(ts_range, filter_index=0) actual_end = get_filter_value_from_json(ts_range, filter_index=1) - expected_begin = "{0}T00:00:00.000Z".format(begin_date_tuple[0]) - expected_end = "{0}T{1}.000Z".format(end_date_tuple_with_time[0], end_date_tuple_with_time[1]) + expected_begin = "{0}T00:00:00.000Z".format(begin_date_list[0]) + expected_end = "{0}T{1}.000Z".format(end_date_list_with_time[0], end_date_list_with_time[1]) assert actual_begin == expected_begin assert actual_end == expected_end diff --git a/tests/securitydata/test_extraction.py b/tests/securitydata/test_extraction.py index 504b6db2b..b5b54e49f 100644 --- a/tests/securitydata/test_extraction.py +++ b/tests/securitydata/test_extraction.py @@ -7,7 +7,7 @@ import code42cli.securitydata.extraction as extraction_module from code42cli.securitydata.options import ExposureType as ExposureTypeOptions -from .conftest import SECURITYDATA_NAMESPACE, begin_date_tuple +from .conftest import SECURITYDATA_NAMESPACE, begin_date_str from ..conftest import get_filter_value_from_json, get_test_date_str @@ -45,7 +45,7 @@ def profile(mocker): @pytest.fixture def namespace_with_begin(namespace): - namespace.begin_date = begin_date_tuple + namespace.begin_date = begin_date_str return namespace @@ -196,46 +196,50 @@ def test_extract_when_not_given_begin_or_advanced_causes_exit(logger, extractor, def test_extract_when_given_begin_date_uses_expected_query(logger, namespace, extractor): - namespace.begin_date = (get_test_date_str(days_ago=89),) + namespace.begin_date = get_test_date_str(days_ago=89) extraction_module.extract(logger, namespace) actual = get_filter_value_from_json(extractor.extract.call_args[0][0], filter_index=0) - expected = "{0}T00:00:00.000Z".format(namespace.begin_date[0]) + expected = "{0}T00:00:00.000Z".format(namespace.begin_date) assert actual == expected def test_extract_when_given_begin_date_and_time_uses_expected_query(logger, namespace, extractor): - namespace.begin_date = (get_test_date_str(days_ago=89), "15:33:02") + date = get_test_date_str(days_ago=89) + time = "15:33:02" + namespace.begin_date = get_test_date_str(days_ago=89) + " " + time extraction_module.extract(logger, namespace) actual = get_filter_value_from_json(extractor.extract.call_args[0][0], filter_index=0) - expected = "{0}T{1}.000Z".format(namespace.begin_date[0], namespace.begin_date[1]) + expected = "{0}T{1}.000Z".format(date, time) assert actual == expected def test_extract_when_given_end_date_uses_expected_query(logger, namespace_with_begin, extractor): - namespace_with_begin.end_date = (get_test_date_str(days_ago=10),) + namespace_with_begin.end_date = get_test_date_str(days_ago=10) extraction_module.extract(logger, namespace_with_begin) actual = get_filter_value_from_json(extractor.extract.call_args[0][0], filter_index=1) - expected = "{0}T23:59:59.000Z".format(namespace_with_begin.end_date[0]) + expected = "{0}T23:59:59.000Z".format(namespace_with_begin.end_date) assert actual == expected def test_extract_when_given_end_date_and_time_uses_expected_query( logger, namespace_with_begin, extractor ): - namespace_with_begin.end_date = (get_test_date_str(days_ago=10), "12:00:11") + date = get_test_date_str(days_ago=10) + time = "12:00:11" + namespace_with_begin.end_date = date + " " + time extraction_module.extract(logger, namespace_with_begin) actual = get_filter_value_from_json(extractor.extract.call_args[0][0], filter_index=1) - expected = "{0}T{1}.000Z".format( - namespace_with_begin.end_date[0], namespace_with_begin.end_date[1] - ) + expected = "{0}T{1}.000Z".format(date, time) assert actual == expected def test_extract_when_using_both_min_and_max_dates_uses_expected_timestamps( logger, namespace, extractor ): - namespace.begin_date = (get_test_date_str(days_ago=89),) - namespace.end_date = (get_test_date_str(days_ago=55), "13:44:44") + end_date = get_test_date_str(days_ago=55) + end_time = "13:44:44" + namespace.begin_date = get_test_date_str(days_ago=89) + namespace.end_date = end_date + " " + end_time extraction_module.extract(logger, namespace) actual_begin_timestamp = get_filter_value_from_json( @@ -244,8 +248,8 @@ def test_extract_when_using_both_min_and_max_dates_uses_expected_timestamps( actual_end_timestamp = get_filter_value_from_json( extractor.extract.call_args[0][0], filter_index=1 ) - expected_begin_timestamp = "{0}T00:00:00.000Z".format(namespace.begin_date[0]) - expected_end_timestamp = "{0}T{1}.000Z".format(namespace.end_date[0], namespace.end_date[1]) + expected_begin_timestamp = "{0}T00:00:00.000Z".format(namespace.begin_date) + expected_end_timestamp = "{0}T{1}.000Z".format(end_date, end_time) assert actual_begin_timestamp == expected_begin_timestamp assert actual_end_timestamp == expected_end_timestamp @@ -255,14 +259,15 @@ def test_extract_when_given_min_timestamp_more_than_ninety_days_back_in_ad_hoc_m logger, namespace, extractor ): namespace.is_incremental = False - namespace.begin_date = (get_test_date_str(days_ago=91), "12:51:00") + date = get_test_date_str(days_ago=91) + " 12:51:00" + namespace.begin_date = date with pytest.raises(SystemExit): extraction_module.extract(logger, namespace) def test_extract_when_end_date_is_before_begin_date_causes_exit(logger, namespace, extractor): - namespace.begin_date = (get_test_date_str(days_ago=5),) - namespace.end_date = (get_test_date_str(days_ago=6),) + namespace.begin_date = get_test_date_str(days_ago=5) + namespace.end_date = get_test_date_str(days_ago=6) with pytest.raises(SystemExit): extraction_module.extract(logger, namespace) @@ -283,7 +288,7 @@ def test_when_given_begin_date_past_90_days_and_is_incremental_and_a_stored_curs def test_when_given_begin_date_and_not_interactive_mode_and_cursor_exists_uses_begin_date( mocker, logger, namespace, extractor ): - namespace.begin_date = (get_test_date_str(days_ago=1),) + namespace.begin_date = get_test_date_str(days_ago=1) namespace.is_incremental = False mock_checkpoint = mocker.patch( "code42cli.securitydata.cursor_store.FileEventCursorStore.get_stored_insertion_timestamp" @@ -292,7 +297,7 @@ def test_when_given_begin_date_and_not_interactive_mode_and_cursor_exists_uses_b extraction_module.extract(logger, namespace) actual_ts = get_filter_value_from_json(extractor.extract.call_args[0][0], filter_index=0) - expected_ts = "{0}T00:00:00.000Z".format(namespace.begin_date[0]) + expected_ts = "{0}T00:00:00.000Z".format(namespace.begin_date) assert actual_ts == expected_ts assert filter_term_is_in_call_args(extractor, EventTimestamp._term) @@ -320,20 +325,6 @@ def test_extract_when_given_invalid_exposure_type_causes_exit(logger, namespace, extraction_module.extract(logger, namespace) -def test_extract_when_given_begin_date_with_len_3_causes_exit(logger, namespace, extractor): - namespace.begin_date = (get_test_date_str(days_ago=5), "12:00:00", "+600") - with pytest.raises(SystemExit): - extraction_module.extract(logger, namespace) - - -def test_extract_when_given_end_date_with_len_3_causes_exit( - logger, namespace_with_begin, extractor -): - namespace_with_begin.end_date = (get_test_date_str(days_ago=5), "12:00:00", "+600") - with pytest.raises(SystemExit): - extraction_module.extract(logger, namespace_with_begin) - - def test_extract_when_given_username_uses_username_filter(logger, namespace_with_begin, extractor): namespace_with_begin.c42usernames = ["test.testerson@example.com"] extraction_module.extract(logger, namespace_with_begin) diff --git a/tests/test_sdk_client.py b/tests/test_sdk_client.py new file mode 100644 index 000000000..a92412960 --- /dev/null +++ b/tests/test_sdk_client.py @@ -0,0 +1,46 @@ +import pytest + +from py42 import debug_level +from py42 import settings + +from code42cli.sdk_client import create_sdk, validate_connection +from .conftest import create_mock_profile + + +@pytest.fixture +def mock_sdk_factory(mocker): + return mocker.patch("py42.sdk.SDK.create_using_local_account") + + +@pytest.fixture +def error_sdk_factory(mocker, mock_sdk_factory): + def side_effect(): + raise Exception() + + mock_sdk_factory.side_effect = side_effect + return mock_sdk_factory + + +def test_create_sdk_when_py42_exception_occurs_causes_exit(error_sdk_factory): + profile = create_mock_profile() + with pytest.raises(SystemExit): + create_sdk(profile, False) + + +def test_create_sdk_when_told_to_debug_turns_on_debug(mock_sdk_factory): + profile = create_mock_profile() + create_sdk(profile, True) + assert settings.debug_level == debug_level.DEBUG + + +def test_validate_connection_when_creating_sdk_raises_returns_false(error_sdk_factory): + assert not validate_connection("Test", "Password", "Authority") + + +def test_validate_connection_when_sdk_does_not_raise_returns_true(mock_sdk_factory): + assert validate_connection("Test", "Password", "Authority") + + +def test_validate_connection_uses_given_credentials(mock_sdk_factory): + assert validate_connection("Authority", "Test", "Password") + mock_sdk_factory.assert_called_once_with("Authority", "Test", "Password")