Skip to content

Commit 3c64b6e

Browse files
committed
feat(commands): reset a user's password
with optional notification to an email address
1 parent 98a0e9a commit 3c64b6e

File tree

5 files changed

+137
-0
lines changed

5 files changed

+137
-0
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from argparse import Namespace
2+
3+
from compiler_admin.commands import RESULT_SUCCESS, RESULT_FAILURE
4+
from compiler_admin.services.google import CallGAMCommand, user_account_name, user_exists
5+
6+
7+
def reset_password(args: Namespace) -> int:
8+
"""Reset a user's password.
9+
10+
Optionally notify an email address with the new randomly generated password.
11+
12+
Args:
13+
username (str): the user account to reset.
14+
15+
notify (str): an email address to send the new password notification.
16+
Returns:
17+
A value indicating if the operation succeeded or failed.
18+
"""
19+
if not hasattr(args, "username"):
20+
raise ValueError("username is required")
21+
22+
account = user_account_name(args.username)
23+
24+
if not user_exists(account):
25+
print(f"User does not exist: {account}")
26+
return RESULT_FAILURE
27+
28+
command = ("update", "user", account, "password", "random")
29+
30+
notify = getattr(args, "notify", None)
31+
if notify:
32+
command += ("notify", notify, "from", "[email protected]")
33+
34+
print(f"User exists, resetting password: {account}")
35+
36+
res = CallGAMCommand(command)
37+
38+
return RESULT_SUCCESS if res == RESULT_SUCCESS else RESULT_FAILURE

compiler_admin/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from compiler_admin.commands.info import info
99
from compiler_admin.commands.init import init
1010
from compiler_admin.commands.offboard import offboard
11+
from compiler_admin.commands.reset_password import reset_password
1112
from compiler_admin.commands.restore import restore
1213
from compiler_admin.commands.signout import signout
1314

@@ -60,6 +61,9 @@ def _subcmd(name, help, add_username_arg=True) -> argparse.ArgumentParser:
6061
"--force", action="store_true", default=False, help="Don't ask for confirmation before offboarding."
6162
)
6263

64+
reset_parser = _subcmd("reset-password", help="Reset a user's password to a randomly generated string.")
65+
reset_parser.add_argument("--notify", help="An email address to send the newly generated password.")
66+
6367
_subcmd("restore", help="Restore an email backup from a prior offboarding.")
6468

6569
signout_parser = _subcmd("signout", help="Signs a user out from all active sessions.")
@@ -86,6 +90,8 @@ def _subcmd(name, help, add_username_arg=True) -> argparse.ArgumentParser:
8690
return offboard(args)
8791
elif args.command == "restore":
8892
return restore(args)
93+
elif args.command == "reset-password":
94+
return reset_password(args)
8995
elif args.command == "signout":
9096
return signout(args)
9197

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from argparse import Namespace
2+
import pytest
3+
4+
from compiler_admin.commands import RESULT_FAILURE, RESULT_SUCCESS
5+
from compiler_admin.commands.reset_password import reset_password, __name__ as MODULE
6+
7+
8+
@pytest.fixture
9+
def mock_google_user_exists(mock_google_user_exists):
10+
return mock_google_user_exists(MODULE)
11+
12+
13+
@pytest.fixture
14+
def mock_google_CallGAMCommand(mock_google_CallGAMCommand):
15+
return mock_google_CallGAMCommand(MODULE)
16+
17+
18+
def test_reset_password_user_username_required():
19+
args = Namespace()
20+
21+
with pytest.raises(ValueError, match="username is required"):
22+
reset_password(args)
23+
24+
25+
def test_reset_password_user_does_not_exist(mock_google_user_exists):
26+
mock_google_user_exists.return_value = False
27+
28+
args = Namespace(username="username")
29+
res = reset_password(args)
30+
31+
assert res == RESULT_FAILURE
32+
33+
34+
def test_reset_password_user_exists(mock_google_user_exists, mock_google_CallGAMCommand):
35+
mock_google_user_exists.return_value = True
36+
37+
args = Namespace(username="username")
38+
res = reset_password(args)
39+
40+
assert res == RESULT_SUCCESS
41+
42+
mock_google_CallGAMCommand.assert_called_once()
43+
call_args = " ".join(mock_google_CallGAMCommand.call_args[0][0])
44+
assert "update user" in call_args
45+
assert "password random" in call_args
46+
47+
48+
def test_reset_password_notify(mock_google_user_exists, mock_google_CallGAMCommand):
49+
mock_google_user_exists.return_value = True
50+
51+
args = Namespace(username="username", notify="[email protected]")
52+
res = reset_password(args)
53+
54+
assert res == RESULT_SUCCESS
55+
56+
mock_google_CallGAMCommand.assert_called_once()
57+
call_args = " ".join(mock_google_CallGAMCommand.call_args[0][0])
58+
assert "update user" in call_args
59+
assert "password random" in call_args
60+
assert "notify [email protected] from [email protected]" in call_args

tests/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ def mock_commands_offboard(mock_module_name):
6464
return mock_module_name("offboard")
6565

6666

67+
@pytest.fixture
68+
def mock_commands_reset_password(mock_module_name):
69+
"""Fixture returns a function that patches commands.reset_password in a given module."""
70+
return mock_module_name("reset_password")
71+
72+
6773
@pytest.fixture
6874
def mock_commands_restore(mock_module_name):
6975
"""Fixture returns a function that patches commands.restore in a given module."""

tests/test_main.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ def mock_commands_offboard(mock_commands_offboard):
3939
return mock_commands_offboard(MODULE)
4040

4141

42+
@pytest.fixture
43+
def mock_commands_reset_password(mock_commands_reset_password):
44+
return mock_commands_reset_password(MODULE)
45+
46+
4247
@pytest.fixture
4348
def mock_commands_restore(mock_commands_restore):
4449
return mock_commands_restore(MODULE)
@@ -177,6 +182,28 @@ def test_main_offboard_no_username(mock_commands_offboard):
177182
assert mock_commands_offboard.call_count == 0
178183

179184

185+
def test_main_reset_password(mock_commands_reset_password):
186+
main(argv=["reset-password", "username"])
187+
188+
mock_commands_reset_password.assert_called_once()
189+
call_args = mock_commands_reset_password.call_args.args
190+
assert Namespace(command="reset-password", username="username", notify=None) in call_args
191+
192+
193+
def test_main_reset_password_notify(mock_commands_reset_password):
194+
main(argv=["reset-password", "username", "--notify", "notification"])
195+
196+
mock_commands_reset_password.assert_called_once()
197+
call_args = mock_commands_reset_password.call_args.args
198+
assert Namespace(command="reset-password", username="username", notify="notification") in call_args
199+
200+
201+
def test_main_reset_password_no_username(mock_commands_reset_password):
202+
with pytest.raises(SystemExit):
203+
main(argv=["reset-password"])
204+
assert mock_commands_reset_password.call_count == 0
205+
206+
180207
def test_main_restore(mock_commands_restore):
181208
main(argv=["restore", "username"])
182209

0 commit comments

Comments
 (0)