Skip to content

Update user #300

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 25, 2021
Merged
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta

## Unreleased

### Added

- New command `code42 users update` to update a single user.

- New command `code42 users bulk update` to update users in bulk.

### Changed

- `code42 alerts search` now uses microsecond precision when searching by alerts' date observed.
Expand Down
2 changes: 2 additions & 0 deletions src/code42cli/click_ext/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from py42.exceptions import Py42LegalHoldNotFoundOrPermissionDeniedError
from py42.exceptions import Py42UpdateClosedCaseError
from py42.exceptions import Py42UserAlreadyAddedError
from py42.exceptions import Py42UsernameMustBeEmailError
from py42.exceptions import Py42UserNotOnListError

from code42cli.errors import Code42CLIError
Expand Down Expand Up @@ -67,6 +68,7 @@ def invoke(self, ctx):
Py42DescriptionLimitExceededError,
Py42CaseAlreadyHasEventError,
Py42UpdateClosedCaseError,
Py42UsernameMustBeEmailError,
) as err:
self.logger.log_error(err)
raise Code42CLIError(str(err))
Expand Down
2 changes: 1 addition & 1 deletion src/code42cli/cmds/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ def bulk(state):
def bulk_deactivate(state, csv_rows, change_device_name, purge_date, format):
"""Deactivate all devices from the provided CSV containing a 'guid' column."""
sdk = state.sdk
csv_rows[0]["deactivated"] = False
csv_rows[0]["deactivated"] = "False"
formatter = OutputFormatter(format, {key: key for key in csv_rows[0].keys()})
for row in csv_rows:
row["change_device_name"] = change_device_name
Expand Down
121 changes: 121 additions & 0 deletions src/code42cli/cmds/users.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import click
from pandas import DataFrame

from code42cli.bulk import generate_template_cmd_factory
from code42cli.bulk import run_bulk_process
from code42cli.click_ext.groups import OrderedGroup
from code42cli.click_ext.options import incompatible_with
from code42cli.errors import Code42CLIError
from code42cli.errors import UserDoesNotExistError
from code42cli.file_readers import read_csv_arg
from code42cli.options import format_option
from code42cli.options import sdk_options
from code42cli.output_formats import DataFrameOutputFormatter
from code42cli.output_formats import OutputFormat
from code42cli.output_formats import OutputFormatter


@click.group(cls=OrderedGroup)
Expand All @@ -33,6 +37,11 @@ def users(state):
)


user_uid_option = click.option(
"--user-id", help="The unique identifier of the user to be modified.", required=True
)


def role_name_option(help):
return click.option("--role-name", help=help)

Expand Down Expand Up @@ -84,6 +93,95 @@ def remove_role(state, username, role_name):
_remove_user_role(state.sdk, role_name, username)


@users.command(name="update")
@user_uid_option
@click.option("--username", help="The new username for the user.")
@click.option("--password", help="The new password for the user.")
@click.option("--email", help="The new email for the user.")
@click.option("--first-name", help="The new first name for the user.")
@click.option("--last-name", help="The new last name for the user.")
@click.option("--notes", help="Notes about this user.")
@click.option(
"--archive-size-quota", help="The total size (in bytes) allowed for this user."
)
@sdk_options()
def update_user(
state,
user_id,
username,
email,
password,
first_name,
last_name,
notes,
archive_size_quota,
):
"""Update a user with the specified unique identifier."""
_update_user(
state.sdk,
user_id,
username,
email,
password,
first_name,
last_name,
notes,
archive_size_quota,
)


_bulk_user_update_headers = [
"user_id",
"username",
"email",
"password",
"first_name",
"last_name",
"notes",
"archive_size_quota",
]


@users.group(cls=OrderedGroup)
@sdk_options(hidden=True)
def bulk(state):
"""Tools for managing users in bulk"""
pass


users_generate_template = generate_template_cmd_factory(
group_name="users",
commands_dict={"update": _bulk_user_update_headers},
help_message="Generate the CSV template needed for bulk user commands.",
)
bulk.add_command(users_generate_template)


@bulk.command(name="update")
@read_csv_arg(headers=_bulk_user_update_headers)
@format_option
@sdk_options()
def bulk_update(state, csv_rows, format):
"""Update a list of users from the provided CSV."""
csv_rows[0]["updated"] = "False"
formatter = OutputFormatter(format, {key: key for key in csv_rows[0].keys()})

def handle_row(**row):
try:
_update_user(
state.sdk, **{key: row[key] for key in row.keys() if key != "updated"}
)
row["updated"] = "True"
except Exception as err:
row["updated"] = f"False: {err}"
return row

result_rows = run_bulk_process(
handle_row, csv_rows, progress_label="Updating users:"
)
formatter.echo_formatted_list(result_rows)


def _add_user_role(sdk, username, role_name):
user_id = _get_user_id(sdk, username)
_get_role_id(sdk, role_name) # function provides role name validation
Expand Down Expand Up @@ -122,3 +220,26 @@ def _get_users_dataframe(sdk, columns, org_uid, role_id, active):
users_list.extend(page["users"])

return DataFrame.from_records(users_list, columns=columns)


def _update_user(
sdk,
user_id,
username,
email,
password,
first_name,
last_name,
notes,
archive_size_quota,
):
return sdk.users.update_user(
user_id,
username=username,
email=email,
password=password,
first_name=first_name,
last_name=last_name,
notes=notes,
archive_size_quota_bytes=archive_size_quota,
)
2 changes: 1 addition & 1 deletion tests/cmds/test_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@ def test_bulk_deactivate_uses_expected_arguments(runner, mocker, cli_state):
assert bulk_processor.call_args[0][1] == [
{
"guid": "test",
"deactivated": False,
"deactivated": "False",
"change_device_name": False,
"purge_date": None,
}
Expand Down
125 changes: 125 additions & 0 deletions tests/cmds/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from code42cli.main import cli


_NAMESPACE = "code42cli.cmds.users"

TEST_ROLE_RETURN_DATA = {
"data": [{"roleName": "Customer Cloud Admin", "roleId": "1234543"}]
}
Expand Down Expand Up @@ -50,6 +52,11 @@ def get_all_users_generator():
yield TEST_USERS_RESPONSE


@pytest.fixture
def update_user_response(mocker):
return _create_py42_response(mocker, "")


@pytest.fixture
def get_available_roles_response(mocker):
return _create_py42_response(mocker, json.dumps(TEST_ROLE_RETURN_DATA))
Expand All @@ -75,6 +82,11 @@ def get_available_roles_success(cli_state, get_available_roles_response):
cli_state.sdk.users.get_available_roles.return_value = get_available_roles_response


@pytest.fixture
def update_user_success(cli_state, update_user_response):
cli_state.sdk.users.update_user.return_value = update_user_response


def test_list_when_non_table_format_outputs_expected_columns(
runner, cli_state, get_all_users_success
):
Expand Down Expand Up @@ -263,3 +275,116 @@ def test_remove_user_role_raises_error_when_username_does_not_exist(
result = runner.invoke(cli, command, obj=cli_state)
assert result.exit_code == 1
assert "User '[email protected]' does not exist." in result.output


def test_update_user_calls_update_user_with_correct_parameters_when_only_some_are_passed(
runner, cli_state, update_user_success
):
command = ["users", "update", "--user-id", "12345", "--email", "test_email"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you include all the args here so the test confirms they all get passed to the right spot?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to test also that it properly passed None where no argument was given (since we don't want the method to accidentally update all unchanged fields to blank values). I've added additional tests to validate that all parameters are passed properly.

runner.invoke(cli, command, obj=cli_state)
cli_state.sdk.users.update_user.assert_called_once_with(
"12345",
username=None,
email="test_email",
password=None,
first_name=None,
last_name=None,
notes=None,
archive_size_quota_bytes=None,
)


def test_update_user_calls_update_user_with_correct_parameters_when_all_are_passed(
runner, cli_state, update_user_success
):
command = [
"users",
"update",
"--user-id",
"12345",
"--email",
"test_email",
"--username",
"test_username",
"--password",
"test_password",
"--first-name",
"test_fname",
"--last-name",
"test_lname",
"--notes",
"test notes",
"--archive-size-quota",
"123456",
]
runner.invoke(cli, command, obj=cli_state)
cli_state.sdk.users.update_user.assert_called_once_with(
"12345",
username="test_username",
email="test_email",
password="test_password",
first_name="test_fname",
last_name="test_lname",
notes="test notes",
archive_size_quota_bytes="123456",
)


def test_bulk_deactivate_uses_expected_arguments_when_only_some_are_passed(
runner, mocker, cli_state
):
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
with runner.isolated_filesystem():
with open("test_bulk_update.csv", "w") as csv:
csv.writelines(
[
"user_id,username,email,password,first_name,last_name,notes,archive_size_quota\n",
"12345,,test_email,,,,,\n",
]
)
runner.invoke(
cli, ["users", "bulk", "update", "test_bulk_update.csv"], obj=cli_state
)
assert bulk_processor.call_args[0][1] == [
{
"user_id": "12345",
"username": "",
"email": "test_email",
"password": "",
"first_name": "",
"last_name": "",
"notes": "",
"archive_size_quota": "",
"updated": "False",
}
]


def test_bulk_deactivate_uses_expected_arguments_when_all_are_passed(
runner, mocker, cli_state
):
bulk_processor = mocker.patch(f"{_NAMESPACE}.run_bulk_process")
with runner.isolated_filesystem():
with open("test_bulk_update.csv", "w") as csv:
csv.writelines(
[
"user_id,username,email,password,first_name,last_name,notes,archive_size_quota\n",
"12345,test_username,test_email,test_pword,test_fname,test_lname,test notes,4321\n",
]
)
runner.invoke(
cli, ["users", "bulk", "update", "test_bulk_update.csv"], obj=cli_state
)
assert bulk_processor.call_args[0][1] == [
{
"user_id": "12345",
"username": "test_username",
"email": "test_email",
"password": "test_pword",
"first_name": "test_fname",
"last_name": "test_lname",
"notes": "test notes",
"archive_size_quota": "4321",
"updated": "False",
}
]