From 12e947835b4bf9d899ef840010b8580e081d5b48 Mon Sep 17 00:00:00 2001 From: Maddie Vargo Date: Thu, 27 May 2021 10:31:56 -0500 Subject: [PATCH 1/8] Command option to report on user legal hold membership and tests --- CHANGELOG.md | 2 + src/code42cli/cmds/users.py | 49 +++++++++++++++++++++- tests/cmds/test_users.py | 83 +++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d18fa71..e5fd1414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta - New command `code42 users remove-role` to remove a user role from a single user. +- New command 'code42 users list --include-legal-hold-membership' to print the legal hold matter name and ID for any user on legal hold + ## 1.6.1 - 2021-05-27 ### Fixed diff --git a/src/code42cli/cmds/users.py b/src/code42cli/cmds/users.py index d368dcca..37e743ee 100644 --- a/src/code42cli/cmds/users.py +++ b/src/code42cli/cmds/users.py @@ -1,5 +1,6 @@ import click from pandas import DataFrame +from pandas import json_normalize from code42cli.click_ext.groups import OrderedGroup from code42cli.click_ext.options import incompatible_with @@ -46,9 +47,19 @@ def username_option(help): @role_name_option("Limit results to only users having the specified role.") @active_option @inactive_option +@click.option( + "--include-legal-hold-membership", + required=False, + type=bool, + default=False, + is_flag=True, + help="Include legal hold membership in output.", +) @format_option @sdk_options() -def list_users(state, org_uid, role_name, active, inactive, format): +def list_users( + state, org_uid, role_name, active, inactive, include_legal_hold_membership, format +): """List users in your Code42 environment.""" if inactive: active = False @@ -59,6 +70,8 @@ def list_users(state, org_uid, role_name, active, inactive, format): else None ) df = _get_users_dataframe(state.sdk, columns, org_uid, role_id, active) + if include_legal_hold_membership: + df = _add_legal_hold_membership_to_user_dataframe(state.sdk, df) if df.empty: click.echo("No results found.") else: @@ -122,3 +135,37 @@ 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 _add_legal_hold_membership_to_user_dataframe(sdk, df): + columns = ["legalHold.legalHoldUid", "legalHold.name", "user.userUid"] + + legal_hold_member_dataframe = ( + json_normalize(list(_get_all_active_hold_memberships(sdk)))[columns] + .groupby(["user.userUid"]) + .agg(",".join) + .rename( + { + "legalHold.legalHoldUid": "legalHoldUid", + "legalHold.name": "legalHoldName", + }, + axis=1, + ) + ) + df = df.merge( + legal_hold_member_dataframe, + how="left", + left_on="userUid", + right_on="user.userUid", + ) + + return df + + +def _get_all_active_hold_memberships(sdk): + for page in sdk.legalhold.get_all_matters(active=True): + for matter in page["legalHolds"]: + for _page in sdk.legalhold.get_all_matter_custodians( + legal_hold_uid=matter["legalHoldUid"], active=True + ): + yield from _page["legalHoldMemberships"] diff --git a/tests/cmds/test_users.py b/tests/cmds/test_users.py index 506fbde7..a2decb93 100644 --- a/tests/cmds/test_users.py +++ b/tests/cmds/test_users.py @@ -1,9 +1,11 @@ import json import pytest +from pandas import DataFrame from py42.response import Py42Response from requests import Response +from code42cli.cmds.users import _add_legal_hold_membership_to_user_dataframe from code42cli.main import cli @@ -32,6 +34,41 @@ } ] } +TEST_MATTER_RESPONSE = { + "legalHolds": [ + {"legalHoldUid": "123456789", "name": "Legal Hold #1", "active": True}, + {"legalHoldUid": "987654321", "name": "Legal Hold #2", "active": True}, + ] +} +TEST_CUSTODIANS_RESPONSE = { + "legalHoldMemberships": [ + { + "legalHoldMembershipUid": "99999", + "active": True, + "creationDate": "2020-07-16T08:50:23.405Z", + "legalHold": {"legalHoldUid": "123456789", "name": "Legal Hold #1"}, + "user": { + "userUid": "911162111513111325", + "username": "test.username@example.com", + "email": "test.username@example.com", + "userExtRef": None, + }, + }, + { + "legalHoldMembershipUid": "11111", + "active": True, + "creationDate": "2020-07-16T08:50:23.405Z", + "legalHold": {"legalHoldUid": "987654321", "name": "Legal Hold #2"}, + "user": { + "userUid": "911162111513111325", + "username": "test.username@example.com", + "email": "test.username@example.com", + "userExtRef": None, + }, + }, + ] +} + TEST_EMPTY_USERS_RESPONSE = {"users": []} TEST_USERNAME = TEST_USERS_RESPONSE["users"][0]["username"] TEST_USER_ID = TEST_USERS_RESPONSE["users"][0]["userId"] @@ -50,6 +87,14 @@ def get_all_users_generator(): yield TEST_USERS_RESPONSE +def matter_list_generator(): + yield TEST_MATTER_RESPONSE + + +def custodian_list_generator(): + yield TEST_CUSTODIANS_RESPONSE + + @pytest.fixture def get_available_roles_response(mocker): return _create_py42_response(mocker, json.dumps(TEST_ROLE_RETURN_DATA)) @@ -70,6 +115,18 @@ def get_user_id_failure(cli_state): cli_state.sdk.users.get_by_username.return_value = TEST_EMPTY_USERS_RESPONSE +@pytest.fixture +def get_all_matter_success(cli_state): + cli_state.sdk.legalhold.get_all_matters.return_value = matter_list_generator() + + +@pytest.fixture +def get_all_custodian_success(cli_state): + cli_state.sdk.legalhold.get_all_matter_custodians.return_value = ( + custodian_list_generator() + ) + + @pytest.fixture def get_available_roles_success(cli_state, get_available_roles_response): cli_state.sdk.users.get_available_roles.return_value = get_available_roles_response @@ -169,6 +226,32 @@ def test_list_users_when_given_excluding_active_and_inactive_uses_active_equals_ ) +def test_add_legal_hold_membership_to_user_dataframe_adds_legal_hold_columns_to_dataframe( + cli_state, get_all_matter_success, get_all_custodian_success +): + testdf = DataFrame.from_records( + [{"userUid": "840103986007089121", "status": "Active"}] + ) + result = _add_legal_hold_membership_to_user_dataframe(cli_state.sdk, testdf) + assert "legalHoldUid" in result.columns + assert "legalHoldName" in result.columns + + +def test_list_include_legal_hold_membership_merges_in_and_concats_legal_hold_info( + runner, + cli_state, + get_all_users_success, + get_all_custodian_success, + get_all_matter_success, +): + result = runner.invoke( + cli, ["users", "list", "--include-legal-hold-membership"], obj=cli_state + ) + + assert "Legal Hold #1,Legal Hold #2" in result.output + assert "123456789,987654321" in result.output + + def test_add_user_role_adds( runner, cli_state, get_user_id_success, get_available_roles_success ): From 002c6053b898f3e8ff5f9c1b490525bc6f912858 Mon Sep 17 00:00:00 2001 From: Maddie Vargo Date: Tue, 6 Jul 2021 15:57:57 -0500 Subject: [PATCH 2/8] Fixing conlicts --- src/code42cli/cmds/users.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/code42cli/cmds/users.py b/src/code42cli/cmds/users.py index 37e743ee..18c526e5 100644 --- a/src/code42cli/cmds/users.py +++ b/src/code42cli/cmds/users.py @@ -169,3 +169,4 @@ def _get_all_active_hold_memberships(sdk): legal_hold_uid=matter["legalHoldUid"], active=True ): yield from _page["legalHoldMemberships"] + From f12fe4565b54eb5b19c2d0b047c1e1de1575a7d2 Mon Sep 17 00:00:00 2001 From: Maddie Vargo Date: Tue, 6 Jul 2021 16:03:41 -0500 Subject: [PATCH 3/8] fix style on users.py --- src/code42cli/cmds/users.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/code42cli/cmds/users.py b/src/code42cli/cmds/users.py index 18c526e5..37e743ee 100644 --- a/src/code42cli/cmds/users.py +++ b/src/code42cli/cmds/users.py @@ -169,4 +169,3 @@ def _get_all_active_hold_memberships(sdk): legal_hold_uid=matter["legalHoldUid"], active=True ): yield from _page["legalHoldMemberships"] - From 7d403d3189f03d9cdec1982bffe1489e7f3aa1ef Mon Sep 17 00:00:00 2001 From: Maddie Vargo Date: Thu, 5 Aug 2021 13:17:11 -0500 Subject: [PATCH 4/8] updated branch to match main --- CHANGELOG.md | 7 +++---- src/code42cli/cmds/users.py | 7 ++----- tests/cmds/test_users.py | 5 ----- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfedcb22..fdea05d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,13 @@ 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. -<<<<<<< HEAD + ## Unreleased ### Added -- New command 'code42 users list --include-legal-hold-membership' to print the legal hold matter name and ID for any user on legal hold -======= +- New command 'code42 users list --include-legal-hold-membership' to print the legal hold matter name and ID for any user on legal hold. + ## 1.8.1 - 2021-07-14 ### Fixed @@ -48,7 +48,6 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta - Now when a user is not found, the error message suggests that it might be because you don't have the necessary permissions. ->>>>>>> df8406052c337416e296326cd7e51ec678e504c6 ## 1.7.0 - 2021-06-17 diff --git a/src/code42cli/cmds/users.py b/src/code42cli/cmds/users.py index 590086b1..6c9519e6 100644 --- a/src/code42cli/cmds/users.py +++ b/src/code42cli/cmds/users.py @@ -65,8 +65,6 @@ def username_option(help, required=False): @inactive_option @click.option( "--include-legal-hold-membership", - required=False, - type=bool, default=False, is_flag=True, help="Include legal hold membership in output.", @@ -315,7 +313,6 @@ def _get_users_dataframe(sdk, columns, org_uid, role_id, active): return DataFrame.from_records(users_list, columns=columns) -<<<<<<< HEAD def _add_legal_hold_membership_to_user_dataframe(sdk, df): columns = ["legalHold.legalHoldUid", "legalHold.name", "user.userUid"] @@ -348,7 +345,8 @@ def _get_all_active_hold_memberships(sdk): legal_hold_uid=matter["legalHoldUid"], active=True ): yield from _page["legalHoldMemberships"] -======= + + def _update_user( sdk, user_id, @@ -381,4 +379,3 @@ def _change_organization(sdk, username, org_id): def _get_org_id(sdk, org_id): org = sdk.orgs.get_by_uid(org_id) return org["orgId"] ->>>>>>> df8406052c337416e296326cd7e51ec678e504c6 diff --git a/tests/cmds/test_users.py b/tests/cmds/test_users.py index 80ffdb7f..5f6a1416 100644 --- a/tests/cmds/test_users.py +++ b/tests/cmds/test_users.py @@ -1,15 +1,10 @@ import pytest -<<<<<<< HEAD from pandas import DataFrame -from py42.response import Py42Response -from requests import Response -======= from py42.exceptions import Py42InvalidEmailError from py42.exceptions import Py42InvalidPasswordError from py42.exceptions import Py42InvalidUsernameError from tests.conftest import create_mock_http_error from tests.conftest import create_mock_response ->>>>>>> df8406052c337416e296326cd7e51ec678e504c6 from code42cli.cmds.users import _add_legal_hold_membership_to_user_dataframe from code42cli.main import cli From ac50108a02f05bb57cac16a84ad20f15b9d52410 Mon Sep 17 00:00:00 2001 From: Maddie Vargo Date: Fri, 6 Aug 2021 15:31:43 -0500 Subject: [PATCH 5/8] Adding changes to meet tests comments --- src/code42cli/cmds/users.py | 6 ++- tests/cmds/test_users.py | 73 ++++++++++++++++++++++++++++++++----- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/code42cli/cmds/users.py b/src/code42cli/cmds/users.py index 6c9519e6..412959f3 100644 --- a/src/code42cli/cmds/users.py +++ b/src/code42cli/cmds/users.py @@ -316,8 +316,12 @@ def _get_users_dataframe(sdk, columns, org_uid, role_id, active): def _add_legal_hold_membership_to_user_dataframe(sdk, df): columns = ["legalHold.legalHoldUid", "legalHold.name", "user.userUid"] + custodians = list(_get_all_active_hold_memberships(sdk)) + if len(custodians) == 0: + return df + legal_hold_member_dataframe = ( - json_normalize(list(_get_all_active_hold_memberships(sdk)))[columns] + json_normalize(custodians)[columns] .groupby(["user.userUid"]) .agg(",".join) .rename( diff --git a/tests/cmds/test_users.py b/tests/cmds/test_users.py index 5f6a1416..dc0ff19a 100644 --- a/tests/cmds/test_users.py +++ b/tests/cmds/test_users.py @@ -1,12 +1,10 @@ import pytest -from pandas import DataFrame from py42.exceptions import Py42InvalidEmailError from py42.exceptions import Py42InvalidPasswordError from py42.exceptions import Py42InvalidUsernameError from tests.conftest import create_mock_http_error from tests.conftest import create_mock_response -from code42cli.cmds.users import _add_legal_hold_membership_to_user_dataframe from code42cli.main import cli from code42cli.worker import WorkerStats @@ -70,7 +68,8 @@ }, ] } - +TEST_EMPTY_CUSTODIANS_RESPONSE = {"legalHoldMemberships": []} +TEST_EMPTY_MATTERS_RESPONSE = {"legalHolds": []} TEST_EMPTY_USERS_RESPONSE = {"users": []} TEST_USERNAME = TEST_USERS_RESPONSE["users"][0]["username"] TEST_USER_ID = TEST_USERS_RESPONSE["users"][0]["userId"] @@ -117,6 +116,14 @@ def custodian_list_generator(): yield TEST_CUSTODIANS_RESPONSE +def empty_custodian_list_generator(): + yield TEST_EMPTY_CUSTODIANS_RESPONSE + + +def empty_matter_list_generator(): + yield TEST_EMPTY_MATTERS_RESPONSE + + @pytest.fixture def update_user_response(mocker): return create_mock_response(mocker) @@ -165,6 +172,18 @@ def get_user_id_failure(mocker, cli_state): ) +@pytest.fixture +def get_custodian_failure(cli_state): + cli_state.sdk.legalhold.get_all_matter_custodians.return_value = ( + empty_custodian_list_generator() + ) + + +@pytest.fixture +def get_matter_failure(cli_state): + cli_state.sdk.legalhold.get_all_matters.return_value = empty_matter_list_generator() + + @pytest.fixture def get_all_matter_success(cli_state): cli_state.sdk.legalhold.get_all_matters.return_value = matter_list_generator() @@ -298,15 +317,49 @@ def test_list_users_when_given_excluding_active_and_inactive_uses_active_equals_ ) -def test_add_legal_hold_membership_to_user_dataframe_adds_legal_hold_columns_to_dataframe( - cli_state, get_all_matter_success, get_all_custodian_success +def test_list_legal_hold_flag_reports_none_for_users_not_on_legal_hold( + runner, + cli_state, + get_all_users_success, + get_custodian_failure, + get_all_matter_success, +): + result = runner.invoke( + cli, + ["users", "list", "--include-legal-hold-membership", "-f", "CSV"], + obj=cli_state, + ) + + assert "Legal Hold #1,Legal Hold #2" not in result.output + assert "123456789,987654321" not in result.output + assert "legalHoldUid" not in result.output + assert "test.username@example.com" in result.output + + +def test_list_legal_hold_flag_reports_none_if_no_matters_exist( + runner, cli_state, get_all_users_success, get_custodian_failure, get_matter_failure ): - testdf = DataFrame.from_records( - [{"userUid": "840103986007089121", "status": "Active"}] + result = runner.invoke( + cli, ["users", "list", "--include-legal-hold-membership"], obj=cli_state ) - result = _add_legal_hold_membership_to_user_dataframe(cli_state.sdk, testdf) - assert "legalHoldUid" in result.columns - assert "legalHoldName" in result.columns + + assert "Legal Hold #1,Legal Hold #2" not in result.output + assert "123456789,987654321" not in result.output + assert "legalHoldUid" not in result.output + assert "test.username@example.com" in result.output + + +def test_list_legal_hold_values_not_included_for_legal_hold_user_if_legal_hold_flag_not_passed( + runner, + cli_state, + get_all_users_success, + get_all_custodian_success, + get_all_matter_success, +): + result = runner.invoke(cli, ["users", "list"], obj=cli_state) + assert "Legal Hold #1,Legal Hold #2" not in result.output + assert "123456789,987654321" not in result.output + assert "test.username@example.com" in result.output def test_list_include_legal_hold_membership_merges_in_and_concats_legal_hold_info( From c5bda5633e5f7928354edbd62ee6722e104f24a6 Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Thu, 26 Aug 2021 11:16:07 -0500 Subject: [PATCH 6/8] use create_mock_respose --- tests/cmds/test_users.py | 43 ++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/tests/cmds/test_users.py b/tests/cmds/test_users.py index dc0ff19a..28fdba0d 100644 --- a/tests/cmds/test_users.py +++ b/tests/cmds/test_users.py @@ -104,26 +104,6 @@ } -def get_all_users_generator(): - yield TEST_USERS_RESPONSE - - -def matter_list_generator(): - yield TEST_MATTER_RESPONSE - - -def custodian_list_generator(): - yield TEST_CUSTODIANS_RESPONSE - - -def empty_custodian_list_generator(): - yield TEST_EMPTY_CUSTODIANS_RESPONSE - - -def empty_matter_list_generator(): - yield TEST_EMPTY_MATTERS_RESPONSE - - @pytest.fixture def update_user_response(mocker): return create_mock_response(mocker) @@ -155,7 +135,10 @@ def get_org_success(cli_state, get_org_response): @pytest.fixture -def get_all_users_success(cli_state): +def get_all_users_success(mocker, cli_state): + def get_all_users_generator(): + yield create_mock_response(mocker, data=TEST_USERS_RESPONSE) + cli_state.sdk.users.get_all.return_value = get_all_users_generator() @@ -173,7 +156,10 @@ def get_user_id_failure(mocker, cli_state): @pytest.fixture -def get_custodian_failure(cli_state): +def get_custodian_failure(mocker, cli_state): + def empty_custodian_list_generator(): + yield TEST_EMPTY_CUSTODIANS_RESPONSE + cli_state.sdk.legalhold.get_all_matter_custodians.return_value = ( empty_custodian_list_generator() ) @@ -181,16 +167,25 @@ def get_custodian_failure(cli_state): @pytest.fixture def get_matter_failure(cli_state): + def empty_matter_list_generator(): + yield TEST_EMPTY_MATTERS_RESPONSE + cli_state.sdk.legalhold.get_all_matters.return_value = empty_matter_list_generator() @pytest.fixture -def get_all_matter_success(cli_state): +def get_all_matter_success(mocker, cli_state): + def matter_list_generator(): + yield create_mock_response(mocker, data=TEST_MATTER_RESPONSE) + cli_state.sdk.legalhold.get_all_matters.return_value = matter_list_generator() @pytest.fixture -def get_all_custodian_success(cli_state): +def get_all_custodian_success(mocker, cli_state): + def custodian_list_generator(): + yield create_mock_response(mocker, data=TEST_CUSTODIANS_RESPONSE) + cli_state.sdk.legalhold.get_all_matter_custodians.return_value = ( custodian_list_generator() ) From aaff514f6cae66c22c85fef3e726cf55152f1f25 Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Thu, 26 Aug 2021 11:19:06 -0500 Subject: [PATCH 7/8] finish using create_mock_response --- tests/cmds/test_users.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/cmds/test_users.py b/tests/cmds/test_users.py index 28fdba0d..74ef2e43 100644 --- a/tests/cmds/test_users.py +++ b/tests/cmds/test_users.py @@ -158,7 +158,7 @@ def get_user_id_failure(mocker, cli_state): @pytest.fixture def get_custodian_failure(mocker, cli_state): def empty_custodian_list_generator(): - yield TEST_EMPTY_CUSTODIANS_RESPONSE + yield create_mock_response(mocker, data=TEST_EMPTY_CUSTODIANS_RESPONSE) cli_state.sdk.legalhold.get_all_matter_custodians.return_value = ( empty_custodian_list_generator() @@ -166,9 +166,9 @@ def empty_custodian_list_generator(): @pytest.fixture -def get_matter_failure(cli_state): +def get_matter_failure(mocker, cli_state): def empty_matter_list_generator(): - yield TEST_EMPTY_MATTERS_RESPONSE + yield create_mock_response(mocker, data=TEST_EMPTY_MATTERS_RESPONSE) cli_state.sdk.legalhold.get_all_matters.return_value = empty_matter_list_generator() From b53ee2b23e10348034ee0d09414ed8833c7750f0 Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Thu, 26 Aug 2021 11:20:45 -0500 Subject: [PATCH 8/8] black --- tests/cmds/test_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cmds/test_users.py b/tests/cmds/test_users.py index 74ef2e43..76265298 100644 --- a/tests/cmds/test_users.py +++ b/tests/cmds/test_users.py @@ -169,7 +169,7 @@ def empty_custodian_list_generator(): def get_matter_failure(mocker, cli_state): def empty_matter_list_generator(): yield create_mock_response(mocker, data=TEST_EMPTY_MATTERS_RESPONSE) - + cli_state.sdk.legalhold.get_all_matters.return_value = empty_matter_list_generator()