diff --git a/SPRINTLOG.md b/SPRINTLOG.md index 7942f82f..29cbf053 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -284,4 +284,8 @@ _Nothing merged in CLI during this sprint_ # 2023-08-07 - 2023-08-18 - Dependency: Bump `PyYAML` to 6.0.1 due to docker issues ([#642](https://github.com/ScilifelabDataCentre/dds_cli/pull/642)) + +# 2023-08-21 - 2023-09-01 + - Print understandable message when request response doesn't contain json ([#638](https://github.com/ScilifelabDataCentre/dds_cli/pull/638)) +- New option in `dds user ls`: `--save-emails` for Super Admins to save emails to file ([#641](https://github.com/ScilifelabDataCentre/dds_cli/pull/641)) diff --git a/dds_cli/__init__.py b/dds_cli/__init__.py index c80193aa..ea059321 100644 --- a/dds_cli/__init__.py +++ b/dds_cli/__init__.py @@ -79,6 +79,7 @@ class DDSEndpoint: USER_ACTIVATION = BASE_ENDPOINT + "/user/activation" USER_ACTIVATE_TOTP = BASE_ENDPOINT + "/user/totp/activate" USER_ACTIVATE_HOTP = BASE_ENDPOINT + "/user/hotp/activate" + USER_EMAILS = BASE_ENDPOINT + "/user/emails" # Authentication - user and project ENCRYPTED_TOKEN = BASE_ENDPOINT + "/user/encrypted_token" diff --git a/dds_cli/__main__.py b/dds_cli/__main__.py index 680dd5a8..8c551333 100644 --- a/dds_cli/__main__.py +++ b/dds_cli/__main__.py @@ -543,14 +543,22 @@ def user_group_command(_): @click.option( "--invites", required=False, is_flag=True, default=False, help="List all current invitations." ) +@click.option( + "--save-emails", + required=False, + is_flag=True, + default=False, + help="[Super Admins only] Save user emails.", +) @click.pass_obj -def list_users(click_ctx, unit, invites): +def list_users(click_ctx, unit, invites, save_emails): """List Unit Admins and Personnel connected to a specific unit. \b Super Admins: - Required to specify a public unit ID. - Can list users within all units. + - Can save list of user emails. \b Unit Admins / Personnel: @@ -564,6 +572,8 @@ def list_users(click_ctx, unit, invites): ) as lister: if invites: lister.list_invites(invites=invites) + elif save_emails: + lister.save_emails() else: lister.list_users(unit=unit) diff --git a/dds_cli/account_manager.py b/dds_cli/account_manager.py index ebfd5280..b3547404 100644 --- a/dds_cli/account_manager.py +++ b/dds_cli/account_manager.py @@ -6,6 +6,7 @@ # Standard library import logging +import pathlib # Installed import rich.markup @@ -274,3 +275,36 @@ def find_user(self, user_to_find: str) -> None: LOG.info( "Account exists: [bold]%s[/bold]", "[blue]Yes[/blue]" if exists else "[red]No[/red]" ) + + def save_emails(self) -> None: + """Get user emails and save them to a text file.""" + # Get emails from API + response, _ = dds_cli.utils.perform_request( + endpoint=dds_cli.DDSEndpoint.USER_EMAILS, + method="get", + headers=self.token, + error_message="Failed getting user emails from the API.", + ) + + # Verify that one of the required pieces of info were returned + empty = response.get("empty") + emails = response.get("emails") + if not empty and not emails: + raise dds_cli.exceptions.ApiResponseError( + "No information returned from the API. Could not get user emails." + ) + + if empty: + LOG.info("There are no user emails to save.") + return + + # Get list of emails + emails = response.get("emails") + LOG.debug("Saving emails to file...") + + # Save emails to file + email_file: pathlib.Path = pathlib.Path("unit_user_emails.txt") + with email_file.open(mode="w+", encoding="utf-8") as file: + file.write("; ".join(emails)) + + LOG.info("Saved emails to file: %s", email_file) diff --git a/tests/test_account_manager.py b/tests/test_account_manager.py index ecd5eef2..d9aaf51f 100644 --- a/tests/test_account_manager.py +++ b/tests/test_account_manager.py @@ -1,3 +1,4 @@ +from _pytest.logging import LogCaptureFixture from pyfakefs.fake_filesystem import FakeFilesystem from dds_cli import account_manager from dds_cli import utils @@ -6,6 +7,8 @@ from dds_cli import DDSEndpoint from dds_cli import exceptions import pytest +import pathlib +import logging from dds_cli.__main__ import LOG @@ -47,3 +50,99 @@ def test_list_users_no_unit_empty_response(fs: FakeFilesystem): assert "The following information was not returned: ['users', 'keys']" in str( exc_info.value ) + + +def test_save_emails_empty_response(fs: FakeFilesystem): + """No file should be created if nothing is returned.""" + # Verify that file doesn't exist + non_existent_file: pathlib.Path = pathlib.Path("unit_user_emails.txt") + assert not fs.exists(file_path=non_existent_file) + + # Empty not returned and empty False + for response_json in [{}, {"empty": False}]: + # Create mocker + with Mocker() as mock: + # Create mocked request - real request not executed + mock.get(DDSEndpoint.USER_EMAILS, status_code=200, json=response_json) + + with pytest.raises(exceptions.ApiResponseError) as exc_info: + # Create accountmanager needed for access and set token to dict + with account_manager.AccountManager(authenticate=False, no_prompt=True) as acm: + acm.token = {} # required, otherwise none + acm.save_emails() # run save emails + + assert "No information returned from the API. Could not get user emails." in str( + exc_info.value + ) + + # Verify that the file still doesn't exist + assert not fs.exists(file_path=non_existent_file) + + +def test_save_emails_no_emails(fs: FakeFilesystem, caplog: LogCaptureFixture): + """No file should be created if nothing is returned.""" + # Verify that file doesn't exist + non_existent_file: pathlib.Path = pathlib.Path("unit_user_emails.txt") + assert not fs.exists(file_path=non_existent_file) + + # Empty not returned and empty False + response_json = {"empty": True} + + # Create mocker + with Mocker() as mock: + # Create mocked request - real request not executed + mock.get(DDSEndpoint.USER_EMAILS, status_code=200, json=response_json) + + with caplog.at_level(logging.INFO): + # Create accountmanager needed for access and set token to dict + with account_manager.AccountManager(authenticate=False, no_prompt=True) as acm: + acm.token = {} # required, otherwise none + acm.save_emails() # run save emails + + assert ( + "dds_cli.account_manager", + logging.INFO, + "There are no user emails to save.", + ) in caplog.record_tuples + + # Verify that the file still doesn't exist + assert not fs.exists(file_path=non_existent_file) + + +def test_save_emails_emails_returned(fs: FakeFilesystem, caplog: LogCaptureFixture): + """No file should be created if nothing is returned.""" + # Verify that file doesn't exist + file_to_save: pathlib.Path = pathlib.Path("unit_user_emails.txt") + assert not fs.exists(file_path=file_to_save) + + # Empty not returned and empty False + response_json = {"emails": ["emailone", "emailtwo", "emailthree", "emailfour"]} + + # Create mocker + with Mocker() as mock: + # Create mocked request - real request not executed + mock.get(DDSEndpoint.USER_EMAILS, status_code=200, json=response_json) + + with caplog.at_level(logging.DEBUG): + # Create accountmanager 'needed for access and set token to dict + with account_manager.AccountManager(authenticate=False, no_prompt=True) as acm: + acm.token = {} # required, otherwise none + acm.save_emails() # run save emails + + assert ( + "dds_cli.account_manager", + logging.DEBUG, + "Saving emails to file...", + ) in caplog.record_tuples + assert ( + "dds_cli.account_manager", + logging.INFO, + f"Saved emails to file: {file_to_save}", + ) in caplog.record_tuples + + # Verify that the file still doesn't exist + assert fs.exists(file_path=file_to_save) + + # Read file and verify contents + with file_to_save.open(mode="r", encoding="utf-8") as f: + assert f.read() == "emailone; emailtwo; emailthree; emailfour"