diff --git a/README.md b/README.md index 8617400..8b7929c 100644 --- a/README.md +++ b/README.md @@ -114,16 +114,17 @@ The following commands are available to work with users in the Compiler domain: ```bash $ compiler-admin user -h -usage: compiler-admin user [-h] {create,convert,delete,offboard,reset-password,restore,signout} ... +usage: compiler-admin user [-h] {alumni,create,convert,delete,offboard,reset,restore,signout} ... positional arguments: - {create,convert,delete,offboard,reset-password,restore,signout} + {alumni,create,convert,delete,offboard,reset,restore,signout} The user command to run. + alumni Convert a user account to a Compiler alumni. create Create a new user in the Compiler domain. convert Convert a user account to a new type. delete Delete a user account. offboard Offboard a user account. - reset-password Reset a user's password to a randomly generated string. + reset Reset a user's password to a randomly generated string. restore Restore an email backup from a prior offboarding. signout Signs a user out from all active sessions. @@ -151,15 +152,17 @@ Additional options are passed through to GAM, see more about [GAM user create](h ```bash $ compiler-admin user convert -h -usage: compiler-admin user convert [-h] username {contractor,partner,staff} +usage: compiler-admin user convert [-h] [--force] [--notify NOTIFY] username {alumni,contractor,partner,staff} positional arguments: username A Compiler user account name, sans domain. - {contractor,partner,staff} + {alumni,contractor,partner,staff} Target account type for this conversion. options: -h, --help show this help message and exit + --force Don't ask for confirmation before conversion. + --notify NOTIFY An email address to send the alumni's new password. ``` ### Offboarding a user diff --git a/compiler_admin/commands/user/__init__.py b/compiler_admin/commands/user/__init__.py index 759c28c..aeb21b1 100644 --- a/compiler_admin/commands/user/__init__.py +++ b/compiler_admin/commands/user/__init__.py @@ -1,10 +1,11 @@ from argparse import Namespace +from compiler_admin.commands.user.alumni import alumni # noqa: F401 from compiler_admin.commands.user.create import create # noqa: F401 from compiler_admin.commands.user.convert import convert # noqa: F401 from compiler_admin.commands.user.delete import delete # noqa: F401 from compiler_admin.commands.user.offboard import offboard # noqa: F401 -from compiler_admin.commands.user.reset_password import reset_password # noqa: F401 +from compiler_admin.commands.user.reset import reset # noqa: F401 from compiler_admin.commands.user.restore import restore # noqa: F401 from compiler_admin.commands.user.signout import signout # noqa: F401 diff --git a/compiler_admin/commands/user/alumni.py b/compiler_admin/commands/user/alumni.py new file mode 100644 index 0000000..d1831f1 --- /dev/null +++ b/compiler_admin/commands/user/alumni.py @@ -0,0 +1,71 @@ +from argparse import Namespace + +from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS +from compiler_admin.commands.user.reset import reset +from compiler_admin.services.google import ( + OU_ALUMNI, + CallGAMCommand, + move_user_ou, + user_account_name, + user_exists, +) + + +def alumni(args: Namespace) -> int: + """Convert a user to a Compiler alumni. + + Optionally notify an email address with the new randomly generated password. + + Args: + username (str): the user account to convert. + + notify (str): an email address to send the new password notification. + Returns: + A value indicating if the operation succeeded or failed. + """ + if not hasattr(args, "username"): + raise ValueError("username is required") + + account = user_account_name(args.username) + + if not user_exists(account): + print(f"User does not exist: {account}") + return RESULT_FAILURE + + if getattr(args, "force", False) is False: + cont = input(f"Convert account to alumni for {account}? (Y/n)") + if not cont.lower().startswith("y"): + print("Aborting conversion.") + return RESULT_SUCCESS + + res = RESULT_SUCCESS + + print("Removing from groups") + res += CallGAMCommand(("user", account, "delete", "groups")) + + print(f"Moving to OU: {OU_ALUMNI}") + res += move_user_ou(account, OU_ALUMNI) + + # reset password, sign out + res += reset(args) + + print("Clearing user profile info") + for prop in ["address", "location", "otheremail", "phone"]: + command = ("update", "user", account, prop, "clear") + res += CallGAMCommand(command) + + print("Resetting recovery email") + recovery = getattr(args, "recovery_email", "") + command = ("update", "user", account, "recoveryemail", recovery) + res += CallGAMCommand(command) + + print("Resetting recovery phone") + recovery = getattr(args, "recovery_phone", "") + command = ("update", "user", account, "recoveryphone", recovery) + res += CallGAMCommand(command) + + print("Turning off 2FA") + command = ("user", account, "turnoff2sv") + res += CallGAMCommand(command) + + return res diff --git a/compiler_admin/commands/user/convert.py b/compiler_admin/commands/user/convert.py index 6e877c1..f078ac2 100644 --- a/compiler_admin/commands/user/convert.py +++ b/compiler_admin/commands/user/convert.py @@ -1,9 +1,11 @@ from argparse import Namespace from compiler_admin import RESULT_SUCCESS, RESULT_FAILURE +from compiler_admin.commands.user.alumni import alumni from compiler_admin.services.google import ( GROUP_PARTNERS, GROUP_STAFF, + OU_ALUMNI, OU_CONTRACTORS, OU_PARTNERS, OU_STAFF, @@ -17,7 +19,7 @@ ) -ACCOUNT_TYPE_OU = {"contractor": OU_CONTRACTORS, "partner": OU_PARTNERS, "staff": OU_STAFF} +ACCOUNT_TYPE_OU = {"alumni": OU_ALUMNI, "contractor": OU_CONTRACTORS, "partner": OU_PARTNERS, "staff": OU_STAFF} def convert(args: Namespace) -> int: @@ -48,7 +50,10 @@ def convert(args: Namespace) -> int: print(f"User exists, converting to: {account_type} for {account}") res = RESULT_SUCCESS - if account_type == "contractor": + if account_type == "alumni": + res = alumni(args) + + elif account_type == "contractor": if user_is_partner(account): res += remove_user_from_group(account, GROUP_PARTNERS) res += remove_user_from_group(account, GROUP_STAFF) diff --git a/compiler_admin/commands/user/offboard.py b/compiler_admin/commands/user/offboard.py index 6b2f19e..f9bd2c5 100644 --- a/compiler_admin/commands/user/offboard.py +++ b/compiler_admin/commands/user/offboard.py @@ -2,8 +2,8 @@ from tempfile import NamedTemporaryFile from compiler_admin import RESULT_SUCCESS, RESULT_FAILURE +from compiler_admin.commands.user.alumni import alumni from compiler_admin.commands.user.delete import delete -from compiler_admin.commands.user.signout import signout from compiler_admin.services.google import ( USER_ARCHIVE, CallGAMCommand, @@ -48,8 +48,7 @@ def offboard(args: Namespace) -> int: print(f"User exists, offboarding: {account}") res = RESULT_SUCCESS - print("Removing from groups") - res += CallGAMCommand(("user", account, "delete", "groups")) + res += alumni(args) print("Backing up email") res += CallGYBCommand(("--service-account", "--email", account, "--action", "backup")) @@ -67,8 +66,6 @@ def offboard(args: Namespace) -> int: res += CallGAMCommand(("user", account, "deprovision", "popimap")) - res += signout(args) - res += delete(args) if alias_account: diff --git a/compiler_admin/commands/user/reset_password.py b/compiler_admin/commands/user/reset.py similarity index 82% rename from compiler_admin/commands/user/reset_password.py rename to compiler_admin/commands/user/reset.py index 7fb9bf1..ecdfdda 100644 --- a/compiler_admin/commands/user/reset_password.py +++ b/compiler_admin/commands/user/reset.py @@ -5,7 +5,7 @@ from compiler_admin.services.google import USER_HELLO, CallGAMCommand, user_account_name, user_exists -def reset_password(args: Namespace) -> int: +def reset(args: Namespace) -> int: """Reset a user's password. Optionally notify an email address with the new randomly generated password. @@ -26,6 +26,12 @@ def reset_password(args: Namespace) -> int: print(f"User does not exist: {account}") return RESULT_FAILURE + if getattr(args, "force", False) is False: + cont = input(f"Reset password for {account}? (Y/n)") + if not cont.lower().startswith("y"): + print("Aborting password reset.") + return RESULT_SUCCESS + command = ("update", "user", account, "password", "random", "changepassword") notify = getattr(args, "notify", None) diff --git a/compiler_admin/main.py b/compiler_admin/main.py index a18b09c..06f2006 100644 --- a/compiler_admin/main.py +++ b/compiler_admin/main.py @@ -133,11 +133,21 @@ def setup_user_command(cmd_parsers: _SubParsersAction): user_cmd.set_defaults(func=user) user_subcmds = add_sub_cmd_parser(user_cmd, help="The user command to run.") + user_alumni = add_sub_cmd_with_username_arg(user_subcmds, "alumni", help="Convert a user account to a Compiler alumni.") + user_alumni.add_argument("--notify", help="An email address to send the alumni's new password.") + user_alumni.add_argument( + "--force", action="store_true", default=False, help="Don't ask for confirmation before conversion." + ) + user_create = add_sub_cmd_with_username_arg(user_subcmds, "create", help="Create a new user in the Compiler domain.") user_create.add_argument("--notify", help="An email address to send the newly created account info.") user_convert = add_sub_cmd_with_username_arg(user_subcmds, "convert", help="Convert a user account to a new type.") user_convert.add_argument("account_type", choices=ACCOUNT_TYPE_OU.keys(), help="Target account type for this conversion.") + user_convert.add_argument( + "--force", action="store_true", default=False, help="Don't ask for confirmation before conversion." + ) + user_convert.add_argument("--notify", help="An email address to send the alumni's new password.") user_delete = add_sub_cmd_with_username_arg(user_subcmds, "delete", help="Delete a user account.") user_delete.add_argument("--force", action="store_true", default=False, help="Don't ask for confirmation before deletion.") @@ -149,8 +159,9 @@ def setup_user_command(cmd_parsers: _SubParsersAction): ) user_reset = add_sub_cmd_with_username_arg( - user_subcmds, "reset-password", help="Reset a user's password to a randomly generated string." + user_subcmds, "reset", help="Reset a user's password to a randomly generated string." ) + user_reset.add_argument("--force", action="store_true", default=False, help="Don't ask for confirmation before reset.") user_reset.add_argument("--notify", help="An email address to send the newly generated password.") add_sub_cmd_with_username_arg(user_subcmds, "restore", help="Restore an email backup from a prior offboarding.") diff --git a/compiler_admin/services/google.py b/compiler_admin/services/google.py index 50ae080..f8873f8 100644 --- a/compiler_admin/services/google.py +++ b/compiler_admin/services/google.py @@ -17,6 +17,7 @@ DOMAIN = "compiler.la" # Org structure +OU_ALUMNI = "alumni" OU_CONTRACTORS = "contractors" OU_STAFF = "staff" OU_PARTNERS = f"{OU_STAFF}/partners" @@ -77,7 +78,7 @@ def add_user_to_group(username: str, group: str) -> int: def move_user_ou(username: str, ou: str) -> int: - """Remove a user from a group.""" + """Move a user into a new OU.""" return CallGAMCommand(("update", "ou", ou, "move", username)) diff --git a/tests/commands/user/test_alumni.py b/tests/commands/user/test_alumni.py new file mode 100644 index 0000000..16df231 --- /dev/null +++ b/tests/commands/user/test_alumni.py @@ -0,0 +1,102 @@ +from argparse import Namespace +import pytest + +from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS +from compiler_admin.commands.user.alumni import alumni, __name__ as MODULE +from compiler_admin.services.google import OU_ALUMNI + + +@pytest.fixture +def mock_commands_reset(mock_commands_reset): + return mock_commands_reset(MODULE) + + +@pytest.fixture +def mock_input_yes(mock_input): + fix = mock_input(MODULE) + fix.return_value = "y" + return fix + + +@pytest.fixture +def mock_input_no(mock_input): + fix = mock_input(MODULE) + fix.return_value = "n" + return fix + + +@pytest.fixture +def mock_google_CallGAMCommand(mock_google_CallGAMCommand): + return mock_google_CallGAMCommand(MODULE) + + +@pytest.fixture +def mock_google_move_user_ou(mock_google_move_user_ou): + return mock_google_move_user_ou(MODULE) + + +@pytest.fixture +def mock_google_remove_user_from_group(mock_google_remove_user_from_group): + return mock_google_remove_user_from_group(MODULE) + + +@pytest.fixture +def mock_google_user_exists(mock_google_user_exists): + return mock_google_user_exists(MODULE) + + +def test_alumni_username_required(): + args = Namespace() + + with pytest.raises(ValueError, match="username is required"): + alumni(args) + + +def test_alumni_user_does_not_exists(mock_google_user_exists, mock_google_CallGAMCommand): + mock_google_user_exists.return_value = False + + args = Namespace(username="username") + res = alumni(args) + + assert res == RESULT_FAILURE + mock_google_CallGAMCommand.assert_not_called() + + +@pytest.mark.usefixtures("mock_input_yes") +def test_alumni_confirm_yes( + mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_reset, mock_google_move_user_ou +): + mock_google_user_exists.return_value = True + + args = Namespace(username="username", force=False) + res = alumni(args) + + assert res == RESULT_SUCCESS + mock_google_CallGAMCommand.assert_called() + mock_google_move_user_ou.assert_called_once_with("username@compiler.la", OU_ALUMNI) + mock_commands_reset.assert_called_once_with(args) + + +@pytest.mark.usefixtures("mock_input_no") +def test_alumni_confirm_no(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_reset, mock_google_move_user_ou): + mock_google_user_exists.return_value = True + + args = Namespace(username="username", force=False) + res = alumni(args) + + assert res == RESULT_SUCCESS + mock_google_CallGAMCommand.assert_not_called() + mock_commands_reset.assert_not_called() + mock_google_move_user_ou.assert_not_called() + + +def test_alumni_force(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_reset, mock_google_move_user_ou): + mock_google_user_exists.return_value = True + + args = Namespace(username="username", force=True) + res = alumni(args) + + assert res == RESULT_SUCCESS + mock_google_CallGAMCommand.assert_called() + mock_google_move_user_ou.assert_called_once_with("username@compiler.la", OU_ALUMNI) + mock_commands_reset.assert_called_once_with(args) diff --git a/tests/commands/user/test_convert.py b/tests/commands/user/test_convert.py index 8440f6b..ddbbc99 100644 --- a/tests/commands/user/test_convert.py +++ b/tests/commands/user/test_convert.py @@ -5,6 +5,11 @@ from compiler_admin.commands.user.convert import convert, __name__ as MODULE +@pytest.fixture +def mock_commands_alumni(mock_commands_alumni): + return mock_commands_alumni(MODULE) + + @pytest.fixture def mock_google_user_exists(mock_google_user_exists): return mock_google_user_exists(MODULE) @@ -98,6 +103,16 @@ def test_convert_user_exists_bad_account_type(mock_google_move_user_ou): assert mock_google_move_user_ou.call_count == 0 +@pytest.mark.usefixtures("mock_google_user_exists_true") +def test_convert_alumni(mock_commands_alumni, mock_google_move_user_ou): + args = Namespace(username="username", account_type="alumni") + res = convert(args) + + assert res == RESULT_SUCCESS + mock_commands_alumni.assert_called_once_with(args) + mock_google_move_user_ou.assert_called_once() + + @pytest.mark.usefixtures( "mock_google_user_exists_true", "mock_google_user_is_partner_false", "mock_google_user_is_staff_false" ) diff --git a/tests/commands/user/test_offboard.py b/tests/commands/user/test_offboard.py index 56caf81..5fa06b0 100644 --- a/tests/commands/user/test_offboard.py +++ b/tests/commands/user/test_offboard.py @@ -25,8 +25,8 @@ def mock_NamedTemporaryFile(mock_NamedTemporaryFile_with_readlines): @pytest.fixture -def mock_commands_signout(mock_commands_signout): - return mock_commands_signout(MODULE) +def mock_commands_alumni(mock_commands_alumni): + return mock_commands_alumni(MODULE) @pytest.fixture @@ -62,7 +62,7 @@ def test_offboard_confirm_yes( mock_google_CallGAMCommand, mock_google_CallGYBCommand, mock_NamedTemporaryFile, - mock_commands_signout, + mock_commands_alumni, mock_commands_delete, ): mock_google_user_exists.return_value = True @@ -75,8 +75,8 @@ def test_offboard_confirm_yes( mock_google_CallGYBCommand.assert_called_once() mock_NamedTemporaryFile.assert_called_once() - mock_commands_signout.assert_called_once() - assert args in mock_commands_signout.call_args.args + mock_commands_alumni.assert_called_once() + assert args in mock_commands_alumni.call_args.args mock_commands_delete.assert_called_once() assert args in mock_commands_delete.call_args.args @@ -87,7 +87,7 @@ def test_offboard_confirm_no( mock_google_user_exists, mock_google_CallGAMCommand, mock_google_CallGYBCommand, - mock_commands_signout, + mock_commands_alumni, mock_commands_delete, ): mock_google_user_exists.return_value = True @@ -99,7 +99,7 @@ def test_offboard_confirm_no( mock_google_CallGAMCommand.assert_not_called() mock_google_CallGYBCommand.assert_not_called() - mock_commands_signout.assert_not_called() + mock_commands_alumni.assert_not_called() mock_commands_delete.assert_not_called() diff --git a/tests/commands/user/test_reset.py b/tests/commands/user/test_reset.py new file mode 100644 index 0000000..b6eb98c --- /dev/null +++ b/tests/commands/user/test_reset.py @@ -0,0 +1,108 @@ +from argparse import Namespace +import pytest + +from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS +from compiler_admin.commands.user.reset import reset, __name__ as MODULE +from compiler_admin.services.google import USER_HELLO + + +@pytest.fixture +def mock_input_yes(mock_input): + fix = mock_input(MODULE) + fix.return_value = "y" + return fix + + +@pytest.fixture +def mock_input_no(mock_input): + fix = mock_input(MODULE) + fix.return_value = "n" + return fix + + +@pytest.fixture +def mock_commands_signout(mock_commands_signout): + return mock_commands_signout(MODULE) + + +@pytest.fixture +def mock_google_user_exists(mock_google_user_exists): + return mock_google_user_exists(MODULE) + + +@pytest.fixture +def mock_google_CallGAMCommand(mock_google_CallGAMCommand): + return mock_google_CallGAMCommand(MODULE) + + +def test_reset_user_username_required(): + args = Namespace() + + with pytest.raises(ValueError, match="username is required"): + reset(args) + + +def test_reset_user_does_not_exist(mock_google_user_exists): + mock_google_user_exists.return_value = False + + args = Namespace(username="username") + res = reset(args) + + assert res == RESULT_FAILURE + + +@pytest.mark.usefixtures("mock_input_yes") +def test_reset_confirm_yes(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout): + mock_google_user_exists.return_value = True + + args = Namespace(username="username", force=False) + res = reset(args) + + assert res == RESULT_SUCCESS + mock_google_CallGAMCommand.assert_called_once() + mock_commands_signout.assert_called_once_with(args) + + +@pytest.mark.usefixtures("mock_input_no") +def test_reset_confirm_no(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout): + mock_google_user_exists.return_value = True + + args = Namespace(username="username", force=False) + res = reset(args) + + assert res == RESULT_SUCCESS + mock_google_CallGAMCommand.assert_not_called() + mock_commands_signout.assert_not_called() + + +def test_reset_user_exists(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout): + mock_google_user_exists.return_value = True + + args = Namespace(username="username", force=True) + res = reset(args) + + assert res == RESULT_SUCCESS + + mock_google_CallGAMCommand.assert_called_once() + call_args = " ".join(mock_google_CallGAMCommand.call_args[0][0]) + assert "update user" in call_args + assert "password random changepassword" in call_args + + mock_commands_signout.assert_called_once_with(args) + + +def test_reset_notify(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout): + mock_google_user_exists.return_value = True + + args = Namespace(username="username", notify="notification@example.com", force=True) + res = reset(args) + + assert res == RESULT_SUCCESS + + mock_google_CallGAMCommand.assert_called_once() + call_args = " ".join(mock_google_CallGAMCommand.call_args[0][0]) + assert "update user" in call_args + assert "password random changepassword" in call_args + assert f"notify notification@example.com from {USER_HELLO}" in call_args + + mock_commands_signout.assert_called_once_with(args) diff --git a/tests/commands/user/test_reset_password.py b/tests/commands/user/test_reset_password.py deleted file mode 100644 index 58ee1d6..0000000 --- a/tests/commands/user/test_reset_password.py +++ /dev/null @@ -1,70 +0,0 @@ -from argparse import Namespace -import pytest - -from compiler_admin import RESULT_FAILURE, RESULT_SUCCESS -from compiler_admin.commands.user.reset_password import reset_password, __name__ as MODULE -from compiler_admin.services.google import USER_HELLO - - -@pytest.fixture -def mock_google_user_exists(mock_google_user_exists): - return mock_google_user_exists(MODULE) - - -@pytest.fixture -def mock_commands_signout(mock_commands_signout): - return mock_commands_signout(MODULE) - - -@pytest.fixture -def mock_google_CallGAMCommand(mock_google_CallGAMCommand): - return mock_google_CallGAMCommand(MODULE) - - -def test_reset_password_user_username_required(): - args = Namespace() - - with pytest.raises(ValueError, match="username is required"): - reset_password(args) - - -def test_reset_password_user_does_not_exist(mock_google_user_exists): - mock_google_user_exists.return_value = False - - args = Namespace(username="username") - res = reset_password(args) - - assert res == RESULT_FAILURE - - -def test_reset_password_user_exists(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout): - mock_google_user_exists.return_value = True - - args = Namespace(username="username") - res = reset_password(args) - - assert res == RESULT_SUCCESS - - mock_google_CallGAMCommand.assert_called_once() - call_args = " ".join(mock_google_CallGAMCommand.call_args[0][0]) - assert "update user" in call_args - assert "password random changepassword" in call_args - - mock_commands_signout.assert_called_once_with(args) - - -def test_reset_password_notify(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout): - mock_google_user_exists.return_value = True - - args = Namespace(username="username", notify="notification@example.com") - res = reset_password(args) - - assert res == RESULT_SUCCESS - - mock_google_CallGAMCommand.assert_called_once() - call_args = " ".join(mock_google_CallGAMCommand.call_args[0][0]) - assert "update user" in call_args - assert "password random changepassword" in call_args - assert f"notify notification@example.com from {USER_HELLO}" in call_args - - mock_commands_signout.assert_called_once_with(args) diff --git a/tests/conftest.py b/tests/conftest.py index 7dedd70..a2ea7e5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,6 +33,12 @@ def mock_input(mock_module_name): return mock_module_name("input") +@pytest.fixture +def mock_commands_alumni(mock_module_name): + """Fixture returns a function that patches the alumni function in a given module.""" + return mock_module_name("alumni") + + @pytest.fixture def mock_commands_create(mock_module_name): """Fixture returns a function that patches the create function in a given module.""" @@ -70,9 +76,9 @@ def mock_commands_offboard(mock_module_name): @pytest.fixture -def mock_commands_reset_password(mock_module_name): - """Fixture returns a function that patches the reset_password command function in a given module.""" - return mock_module_name("reset_password") +def mock_commands_reset(mock_module_name): + """Fixture returns a function that patches the reset command function in a given module.""" + return mock_module_name("reset") @pytest.fixture diff --git a/tests/test_main.py b/tests/test_main.py index a6a9421..32a0f0c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -252,6 +252,53 @@ def test_main_time_convert_output(mock_commands_time): ) +def test_main_user_alumni(mock_commands_user): + main(argv=["user", "alumni", "username"]) + + mock_commands_user.assert_called_once() + call_args = mock_commands_user.call_args.args + assert ( + Namespace(func=mock_commands_user, command="user", subcommand="alumni", username="username", force=False, notify=None) + in call_args + ) + + +def test_main_user_alumni_notify(mock_commands_user): + main(argv=["user", "alumni", "username", "--notify", "notification"]) + + mock_commands_user.assert_called_once() + call_args = mock_commands_user.call_args.args + assert ( + Namespace( + func=mock_commands_user, + command="user", + subcommand="alumni", + username="username", + force=False, + notify="notification", + ) + in call_args + ) + + +def test_main_user_alumni_force(mock_commands_user): + main(argv=["user", "alumni", "username", "--force"]) + + mock_commands_user.assert_called_once() + call_args = mock_commands_user.call_args.args + assert ( + Namespace( + func=mock_commands_user, + command="user", + subcommand="alumni", + username="username", + force=True, + notify=None, + ) + in call_args + ) + + def test_main_user_create(mock_commands_user): main(argv=["user", "create", "username"]) @@ -298,7 +345,13 @@ def test_main_user_convert(mock_commands_user): call_args = mock_commands_user.call_args.args assert ( Namespace( - func=mock_commands_user, command="user", subcommand="convert", username="username", account_type="contractor" + func=mock_commands_user, + command="user", + subcommand="convert", + username="username", + account_type="contractor", + force=False, + notify=None, ) in call_args ) @@ -388,33 +441,49 @@ def test_main_user_offboard_no_username(mock_commands_user): assert mock_commands_user.call_count == 0 -def test_main_user_reset_password(mock_commands_user): - main(argv=["user", "reset-password", "username"]) +def test_main_user_reset(mock_commands_user): + main(argv=["user", "reset", "username"]) mock_commands_user.assert_called_once() call_args = mock_commands_user.call_args.args assert ( - Namespace(func=mock_commands_user, command="user", subcommand="reset-password", username="username", notify=None) + Namespace(func=mock_commands_user, command="user", subcommand="reset", username="username", force=False, notify=None) in call_args ) -def test_main_user_reset_password_notify(mock_commands_user): - main(argv=["user", "reset-password", "username", "--notify", "notification"]) +def test_main_user_reset_notify(mock_commands_user): + main(argv=["user", "reset", "username", "--notify", "notification"]) mock_commands_user.assert_called_once() call_args = mock_commands_user.call_args.args assert ( Namespace( - func=mock_commands_user, command="user", subcommand="reset-password", username="username", notify="notification" + func=mock_commands_user, + command="user", + subcommand="reset", + username="username", + notify="notification", + force=False, ) in call_args ) -def test_main_user_reset_password_no_username(mock_commands_user): +def test_main_user_reset_force(mock_commands_user): + main(argv=["user", "reset", "username", "--force"]) + + mock_commands_user.assert_called_once() + call_args = mock_commands_user.call_args.args + assert ( + Namespace(func=mock_commands_user, command="user", subcommand="reset", username="username", force=True, notify=None) + in call_args + ) + + +def test_main_user_reset_no_username(mock_commands_user): with pytest.raises(SystemExit): - main(argv=["user", "reset-password"]) + main(argv=["user", "reset"]) assert mock_commands_user.call_count == 0