Skip to content

Commit decef45

Browse files
authored
Feat: command to reset a user's password (#15)
2 parents 98a0e9a + 567632a commit decef45

File tree

7 files changed

+155
-2
lines changed

7 files changed

+155
-2
lines changed

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@ Built on top of [GAMADV-XTD3](https://github.com/taers232c/GAMADV-XTD3) and [GYB
1010

1111
```bash
1212
$ compiler-admin -h
13-
usage: compiler-admin [-h] [-v] {info,init,create,convert,delete,offboard,restore,signout} ...
13+
usage: compiler-admin [-h] [-v] {info,init,create,convert,delete,offboard,reset-password,restore,signout} ...
1414

1515
positional arguments:
16-
{info,init,create,convert,delete,offboard,restore,signout}
16+
{info,init,create,convert,delete,offboard,reset-password,restore,signout}
1717
info Print configuration and debugging information.
1818
init Initialize a new admin project. This command should be run once before any others.
1919
create Create a new user in the Compiler domain.
2020
convert Convert a user account to a new type.
2121
delete Delete a user account.
2222
offboard Offboard a user account.
23+
reset-password Reset a user's password to a randomly generated string.
2324
restore Restore an email backup from a prior offboarding.
2425
signout Signs a user out from all active sessions.
2526
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from argparse import Namespace
2+
3+
from compiler_admin.commands import RESULT_SUCCESS, RESULT_FAILURE
4+
from compiler_admin.commands.signout import signout
5+
from compiler_admin.services.google import USER_HELLO, CallGAMCommand, user_account_name, user_exists
6+
7+
8+
def reset_password(args: Namespace) -> int:
9+
"""Reset a user's password.
10+
11+
Optionally notify an email address with the new randomly generated password.
12+
13+
Args:
14+
username (str): the user account to reset.
15+
16+
notify (str): an email address to send the new password notification.
17+
Returns:
18+
A value indicating if the operation succeeded or failed.
19+
"""
20+
if not hasattr(args, "username"):
21+
raise ValueError("username is required")
22+
23+
account = user_account_name(args.username)
24+
25+
if not user_exists(account):
26+
print(f"User does not exist: {account}")
27+
return RESULT_FAILURE
28+
29+
command = ("update", "user", account, "password", "random", "changepassword")
30+
31+
notify = getattr(args, "notify", None)
32+
if notify:
33+
command += ("notify", notify, "from", USER_HELLO)
34+
35+
print(f"User exists, resetting password: {account}")
36+
37+
res = CallGAMCommand(command)
38+
res += signout(args)
39+
40+
return RESULT_SUCCESS if res == RESULT_SUCCESS else RESULT_FAILURE

compiler_admin/main.py

+6
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

compiler_admin/services/google.py

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ def user_account_name(username: str) -> str:
3838
# Archive account
3939
USER_ARCHIVE = user_account_name("archive")
4040

41+
# Hello account
42+
USER_HELLO = user_account_name("hello")
43+
4144
# Groups
4245
GROUP_PARTNERS = user_account_name("partners")
4346
GROUP_STAFF = user_account_name("staff")

tests/commands/test_reset_password.py

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
from compiler_admin.services.google import USER_HELLO
7+
8+
9+
@pytest.fixture
10+
def mock_google_user_exists(mock_google_user_exists):
11+
return mock_google_user_exists(MODULE)
12+
13+
14+
@pytest.fixture
15+
def mock_commands_signout(mock_commands_signout):
16+
return mock_commands_signout(MODULE)
17+
18+
19+
@pytest.fixture
20+
def mock_google_CallGAMCommand(mock_google_CallGAMCommand):
21+
return mock_google_CallGAMCommand(MODULE)
22+
23+
24+
def test_reset_password_user_username_required():
25+
args = Namespace()
26+
27+
with pytest.raises(ValueError, match="username is required"):
28+
reset_password(args)
29+
30+
31+
def test_reset_password_user_does_not_exist(mock_google_user_exists):
32+
mock_google_user_exists.return_value = False
33+
34+
args = Namespace(username="username")
35+
res = reset_password(args)
36+
37+
assert res == RESULT_FAILURE
38+
39+
40+
def test_reset_password_user_exists(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout):
41+
mock_google_user_exists.return_value = True
42+
43+
args = Namespace(username="username")
44+
res = reset_password(args)
45+
46+
assert res == RESULT_SUCCESS
47+
48+
mock_google_CallGAMCommand.assert_called_once()
49+
call_args = " ".join(mock_google_CallGAMCommand.call_args[0][0])
50+
assert "update user" in call_args
51+
assert "password random changepassword" in call_args
52+
53+
mock_commands_signout.assert_called_once_with(args)
54+
55+
56+
def test_reset_password_notify(mock_google_user_exists, mock_google_CallGAMCommand, mock_commands_signout):
57+
mock_google_user_exists.return_value = True
58+
59+
args = Namespace(username="username", notify="[email protected]")
60+
res = reset_password(args)
61+
62+
assert res == RESULT_SUCCESS
63+
64+
mock_google_CallGAMCommand.assert_called_once()
65+
call_args = " ".join(mock_google_CallGAMCommand.call_args[0][0])
66+
assert "update user" in call_args
67+
assert "password random changepassword" in call_args
68+
assert f"notify [email protected] from {USER_HELLO}" in call_args
69+
70+
mock_commands_signout.assert_called_once_with(args)

tests/conftest.py

+6
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

+27
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)