Skip to content

Commit 07c846f

Browse files
author
Juliya Smith
authored
Feature/update profile (#29)
1 parent 2c64d16 commit 07c846f

9 files changed

Lines changed: 303 additions & 167 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
1010

1111
## Unreleased
1212

13-
### Changes
13+
### Changed
1414

1515
- `securitydata` renamed to `security-data`.
1616
- From `security-data` related subcommands (such as `print`):
@@ -22,6 +22,7 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
2222
### Added
2323

2424
- `code42 profile create` command.
25+
- `code42 profile update` command.
2526

2627
### Removed
2728

src/code42cli/cmds/profile.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,18 @@ def load_subcommands():
4848
u"Create profile settings. The first profile created will be the default.",
4949
u"{} {}".format(usage_prefix, u"create <profile-name> <server-address> <username>"),
5050
handler=create_profile,
51-
arg_customizer=_load_profile_create_descripions,
51+
arg_customizer=_load_profile_create_descriptions,
5252
)
5353

54-
return [show, list_all, use, reset_pw, create]
54+
update = Command(
55+
u"update",
56+
u"Update an existing profile.",
57+
u"{} {}".format(usage_prefix, u"update <optional args>"),
58+
handler=update_profile,
59+
arg_customizer=_load_profile_update_descriptions,
60+
)
61+
62+
return [show, list_all, use, reset_pw, create, update]
5563

5664

5765
def show_profile(profile=None):
@@ -67,27 +75,31 @@ def show_profile(profile=None):
6775

6876

6977
def create_profile(profile, server, username, disable_ssl_errors=False):
70-
"""Sets the given profile using command line arguments."""
71-
if cliprofile.profile_exists(profile):
72-
print_error(u"A profile named {} already exists.".format(profile))
73-
exit(1)
74-
7578
cliprofile.create_profile(profile, server, username, disable_ssl_errors)
7679
_prompt_for_allow_password_set(profile)
7780

7881

82+
def update_profile(profile=None, server=None, username=None, disable_ssl_errors=None):
83+
profile = cliprofile.get_profile(profile)
84+
cliprofile.update_profile(profile.name, server, username, disable_ssl_errors)
85+
_prompt_for_allow_password_set(profile.name)
86+
87+
7988
def prompt_for_password_reset(profile=None):
8089
"""Securely prompts for your password and then stores it using keyring."""
8190
c42profile = cliprofile.get_profile(profile)
8291
new_password = getpass()
92+
_validate_connection(c42profile.authority_url, c42profile.username, new_password)
93+
cliprofile.set_password(new_password, c42profile.name)
8394

84-
if not validate_connection(c42profile.authority_url, c42profile.username, new_password):
95+
96+
def _validate_connection(authority, username, password):
97+
if not validate_connection(authority, username, password):
8598
print_error(
8699
u"Your credentials failed to validate, so your password was not stored."
87100
u"Check your network connection and the spelling of your username and server URL."
88101
)
89102
exit(1)
90-
cliprofile.set_password(new_password, c42profile.name)
91103

92104

93105
def list_profiles(*args):
@@ -110,13 +122,24 @@ def _load_profile_description(argument_collection):
110122
profile.set_help(PROFILE_HELP)
111123

112124

113-
def _load_profile_create_descripions(argument_collection):
125+
def _load_profile_create_descriptions(argument_collection):
114126
profile = argument_collection.arg_configs["profile"]
127+
profile.set_help(u"The name to give the profile being created.")
128+
_load_profile_settings_descriptions(argument_collection)
129+
130+
131+
def _load_profile_update_descriptions(argument_collection):
132+
profile = argument_collection.arg_configs["profile"]
133+
profile.set_help(u"The name to give the profile being updated.")
134+
_load_profile_settings_descriptions(argument_collection)
135+
argument_collection.arg_configs["server"].add_short_option_name("-s")
136+
argument_collection.arg_configs["username"].add_short_option_name("-u")
137+
138+
139+
def _load_profile_settings_descriptions(argument_collection):
115140
server = argument_collection.arg_configs["server"]
116141
username = argument_collection.arg_configs["username"]
117-
118142
disable_ssl_errors = argument_collection.arg_configs["disable_ssl_errors"]
119-
profile.set_help(u"The name to give the profile being created.")
120143
server.set_help(u"The url and port of the Code42 server.")
121144
username.set_help(u"The username of the Code42 API user.")
122145
disable_ssl_errors.set_help(

src/code42cli/cmds/securitydata/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def __iter__(self):
4646
class SearchArguments(object):
4747
"""These string values should match `argparse` stored parameter names. For example, for the
4848
CLI argument `--c42-username`, the string should be `c42_username`."""
49+
4950
ADVANCED_QUERY = u"advanced_query"
5051
BEGIN_DATE = u"begin"
5152
END_DATE = u"end"

src/code42cli/commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Command(object):
2222
2323
description (str or unicode): Descriptive text to be displayed when using -h.
2424
25-
usage (str, optional): A usage example to be displayed when using -h.
25+
usage (str or unicode, optional): A usage example to be displayed when using -h.
2626
handler (function, optional): The function to be exectued when the command is run.
2727
2828
arg_customizer (function, optional): A function accepting a single `ArgCollection`

src/code42cli/config.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
from code42cli.compat import str
88

99

10+
class NoConfigProfileError(Exception):
11+
def __init__(self):
12+
super(Exception, self).__init__(u"Profile does not exist.")
13+
14+
1015
class ConfigAccessor(object):
1116
DEFAULT_VALUE = u"__DEFAULT__"
1217
AUTHORITY_KEY = u"c42_authority_url"
@@ -31,9 +36,9 @@ def get_profile(self, name=None):
3136
If the name does not exist or there is no existing profile, it will throw an exception.
3237
"""
3338
name = name or self._default_profile_name
34-
if name not in self.parser.sections() or name == self.DEFAULT_VALUE:
35-
raise Exception(u"Profile does not exist.")
36-
return self.parser[name]
39+
if name not in self._get_sections() or name == self.DEFAULT_VALUE:
40+
raise NoConfigProfileError()
41+
return self._get_profile(name)
3742

3843
def get_all_profiles(self):
3944
"""Returns all the available profiles."""
@@ -47,21 +52,30 @@ def create_profile(self, name, server, username, ignore_ssl_errors):
4752
"""Creates a new profile if one does not already exist for that name."""
4853
try:
4954
self.get_profile(name)
50-
except Exception as ex:
55+
except NoConfigProfileError as ex:
5156
if name is not None and name != self.DEFAULT_VALUE:
5257
self._create_profile_section(name)
5358
else:
5459
raise ex
55-
profile = self.parser[name]
56-
self._set_authority_url(server, profile)
57-
self._set_username(username, profile)
58-
self._set_ignore_ssl_errors(ignore_ssl_errors, profile)
60+
61+
profile = self.get_profile(name)
62+
self.update_profile(profile.name, server, username, ignore_ssl_errors)
5963
self._try_complete_setup(profile)
6064

65+
def update_profile(self, name, server=None, username=None, ignore_ssl_errors=None):
66+
profile = self.get_profile(name)
67+
if server:
68+
self._set_authority_url(server, profile)
69+
if username:
70+
self._set_username(username, profile)
71+
if ignore_ssl_errors is not None:
72+
self._set_ignore_ssl_errors(ignore_ssl_errors, profile)
73+
self._save()
74+
6175
def switch_default_profile(self, new_default_name):
6276
"""Changes what is marked as the default profile in the internal section."""
6377
if self.get_profile(new_default_name) is None:
64-
raise Exception(u"Profile does not exist.")
78+
raise NoConfigProfileError()
6579
self._internal[self.DEFAULT_PROFILE] = new_default_name
6680
self._save()
6781
print(u"{} has been set as the default profile.".format(new_default_name))
@@ -75,6 +89,12 @@ def _set_username(self, new_value, profile):
7589
def _set_ignore_ssl_errors(self, new_value, profile):
7690
profile[self.IGNORE_SSL_ERRORS_KEY] = str(new_value)
7791

92+
def _get_sections(self):
93+
return self.parser.sections()
94+
95+
def _get_profile(self, name):
96+
return self.parser[name]
97+
7898
@property
7999
def _internal(self):
80100
return self.parser[self._INTERNAL_SECTION]
@@ -84,7 +104,7 @@ def _default_profile_name(self):
84104
return self._internal[self.DEFAULT_PROFILE]
85105

86106
def _get_profile_names(self):
87-
names = list(self.parser.sections())
107+
names = list(self._get_sections())
88108
names.remove(self._INTERNAL_SECTION)
89109
return names
90110

src/code42cli/profile.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import code42cli.password as password
2-
from code42cli.config import ConfigAccessor, config_accessor
2+
from code42cli.config import ConfigAccessor, config_accessor, NoConfigProfileError
33
from code42cli.util import print_error, print_create_profile_help
44

55

@@ -23,6 +23,11 @@ def username(self):
2323
def ignore_ssl_errors(self):
2424
return self._profile[ConfigAccessor.IGNORE_SSL_ERRORS_KEY]
2525

26+
@property
27+
def has_stored_password(self):
28+
stored_password = password.get_stored_password(self)
29+
return stored_password is not None and stored_password != u""
30+
2631
def get_password(self):
2732
pwd = password.get_stored_password(self)
2833
if not pwd:
@@ -37,13 +42,14 @@ def __str__(self):
3742

3843
def _get_profile(profile_name=None):
3944
"""Returns the profile for the given name."""
40-
return Code42Profile(config_accessor.get_profile(profile_name))
45+
config_profile = config_accessor.get_profile(profile_name)
46+
return Code42Profile(config_profile)
4147

4248

4349
def get_profile(profile_name=None):
4450
try:
4551
return _get_profile(profile_name)
46-
except Exception as ex:
52+
except NoConfigProfileError as ex:
4753
print_error(str(ex))
4854
print_create_profile_help()
4955
exit(1)
@@ -53,15 +59,15 @@ def default_profile_exists():
5359
try:
5460
profile = _get_profile()
5561
return profile.name and profile.name != ConfigAccessor.DEFAULT_VALUE
56-
except Exception:
62+
except NoConfigProfileError:
5763
return False
5864

5965

6066
def profile_exists(profile_name=None):
6167
try:
6268
_get_profile(profile_name)
6369
return True
64-
except Exception:
70+
except NoConfigProfileError:
6571
return False
6672

6773

@@ -70,9 +76,17 @@ def switch_default_profile(profile_name):
7076

7177

7278
def create_profile(name, server, username, ignore_ssl_errors):
79+
if profile_exists(name):
80+
print_error(u"A profile named {} already exists.".format(name))
81+
exit(1)
82+
7383
config_accessor.create_profile(name, server, username, ignore_ssl_errors)
7484

7585

86+
def update_profile(name, server, username, ignore_ssl_errors):
87+
config_accessor.update_profile(name, server, username, ignore_ssl_errors)
88+
89+
7690
def get_all_profiles():
7791
profiles = [Code42Profile(profile) for profile in config_accessor.get_all_profiles()]
7892
return profiles

tests/cmds/test_profile.py

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ def mock_verify(mocker):
3434
return mocker.patch("code42cli.cmds.profile.validate_connection")
3535

3636

37+
@pytest.fixture
38+
def valid_connection(mock_verify):
39+
mock_verify.return_value = True
40+
return mock_verify
41+
42+
43+
@pytest.fixture
44+
def invalid_connection(mock_verify):
45+
mock_verify.return_value = False
46+
return mock_verify
47+
48+
3749
def test_show_profile_outputs_profile_info(capsys, mock_cliprofile_namespace, profile):
3850
profile.name = "testname"
3951
profile.authority_url = "example.com"
@@ -58,18 +70,6 @@ def test_show_profile_when_password_set_outputs_password_note(
5870
assert "A password is set" not in capture.out
5971

6072

61-
def test_create_profile_if_profile_exists_exits(capsys, mock_cliprofile_namespace):
62-
mock_cliprofile_namespace.profile_exists.return_value = True
63-
success = True
64-
try:
65-
profilecmd.create_profile("foo", "bar", "baz", True)
66-
except SystemExit:
67-
success = True
68-
capture = capsys.readouterr()
69-
assert "already exists" in capture.out
70-
assert success
71-
72-
7373
def test_create_profile_if_user_sets_password_is_created(
7474
user_agreement, mock_verify, mock_cliprofile_namespace
7575
):
@@ -86,7 +86,7 @@ def test_create_profile_if_user_does_not_set_password_is_created(
8686
mock_cliprofile_namespace.create_profile.assert_called_once_with("foo", "bar", "baz", True)
8787

8888

89-
def test_create_profile_if_user_does_not_set_password_does_not_save_password(
89+
def test_create_profile_if_user_does_not_agree_does_not_save_password(
9090
user_disagreement, mock_verify, mock_cliprofile_namespace
9191
):
9292
mock_cliprofile_namespace.profile_exists.return_value = False
@@ -95,9 +95,8 @@ def test_create_profile_if_user_does_not_set_password_does_not_save_password(
9595

9696

9797
def test_create_profile_if_credentials_invalid_password_not_saved(
98-
user_agreement, mock_verify, mock_cliprofile_namespace
98+
user_agreement, invalid_connection, mock_cliprofile_namespace
9999
):
100-
mock_verify.return_value = False
101100
mock_cliprofile_namespace.profile_exists.return_value = False
102101
success = False
103102
try:
@@ -109,14 +108,60 @@ def test_create_profile_if_credentials_invalid_password_not_saved(
109108

110109

111110
def test_create_profile_if_credentials_valid_password_saved(
112-
mocker, user_agreement, mock_verify, mock_cliprofile_namespace
111+
mocker, user_agreement, valid_connection, mock_cliprofile_namespace
113112
):
114-
mock_verify.return_value = True
115113
mock_cliprofile_namespace.profile_exists.return_value = False
116114
profilecmd.create_profile("foo", "bar", "baz", True)
117115
mock_cliprofile_namespace.set_password.assert_called_once_with("newpassword", mocker.ANY)
118116

119117

118+
def test_update_profile_updates_existing_profile(
119+
mock_cliprofile_namespace, user_agreement, valid_connection, profile
120+
):
121+
name = "foo"
122+
profile.name = name
123+
mock_cliprofile_namespace.get_profile.return_value = profile
124+
125+
profilecmd.update_profile(name, "bar", "baz", True)
126+
mock_cliprofile_namespace.update_profile.assert_called_once_with(name, "bar", "baz", True)
127+
128+
129+
def test_update_profile_if_user_does_not_agree_does_not_save_password(
130+
mock_cliprofile_namespace, user_disagreement, invalid_connection, profile
131+
):
132+
name = "foo"
133+
profile.name = name
134+
mock_cliprofile_namespace.get_profile.return_value = profile
135+
assert not mock_cliprofile_namespace.set_password.call_count
136+
137+
138+
def test_update_profile_if_credentials_invalid_password_not_saved(
139+
user_agreement, invalid_connection, mock_cliprofile_namespace, profile
140+
):
141+
name = "foo"
142+
profile.name = name
143+
mock_cliprofile_namespace.get_profile.return_value = profile
144+
145+
success = False
146+
try:
147+
profilecmd.create_profile("foo", "bar", "baz", True)
148+
except SystemExit:
149+
success = True
150+
assert not mock_cliprofile_namespace.set_password.call_count
151+
assert success
152+
153+
154+
def test_update_profile_if_user_agrees_and_valid_connection_sets_password(
155+
mocker, user_agreement, valid_connection, mock_cliprofile_namespace, profile
156+
):
157+
name = "foo"
158+
profile.name = name
159+
mock_cliprofile_namespace.get_profile.return_value = profile
160+
161+
profilecmd.update_profile(name, "bar", "baz", True)
162+
mock_cliprofile_namespace.set_password.assert_called_once_with("newpassword", mocker.ANY)
163+
164+
120165
def test_prompt_for_password_reset_if_credentials_valid_password_saved(
121166
mocker, user_agreement, mock_verify, mock_cliprofile_namespace
122167
):

0 commit comments

Comments
 (0)