diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a6d60f022..73ed4d444 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,79 +1,94 @@ -## Before submitting this PR +## Read this before submitting the PR -1. **Description:** _Add a summary of the changes in this PR and the related issue._ -2. **Jira task / GitHub issue:** _Link to the github issue or add the Jira task ID here._ -3. **How to test:** _Add information on how someone could manually test this functionality. As detailed as possible._ -4. **Type of change:** [_Check the relevant boxes in the section below_](#what-type-of-changes-does-the-pr-contain) -5. **Add docstrings and comments to code**, _even if_ you personally think it's obvious. +1. Always create a Draft PR first +2. Go through sections 1-5 below, fill them in and check all the boxes +3. Make sure that the branch is updated; if there's an "Update branch" button at the bottom of the PR, rebase or update branch. +4. When all boxes are checked, information is filled in, and the branch is updated: mark as Ready For Review and tag reviewers (top right) +5. Once there is a submitted review, implement the suggestions (if reasonable, otherwise discuss) and request an new review. -## What _type of change(s)_ does the PR contain? +If there is a field which you are unsure about, enter the edit mode of this description or go to the [PR template](https://github.com/ScilifelabDataCentre/dds_cli/blob/dev/.github/pull_request_template.md); There are invisible comments providing descriptions which may be of help. - +## 1. Description / Summary + +_Add a summary of the changes in this PR and the related issue._ + +## 2. Jira task / GitHub issue + +_Link to the github issue or add the Jira task ID here._ + +## 3. Type of change + +What _type of change(s)_ does the PR contain? + +**Check the relevant boxes below. For an explanation of the different sections, enter edit mode of this PR description template.** - [ ] New feature - - [ ] Breaking: _Please describe the reason for the break and how we can fix it._ - - [ ] Non-breaking -- [ ] Bug fix - - [ ] Breaking: _Please describe the reason for the break and how we can fix it._ - - [ ] Non-breaking -- [ ] Security Alert fix -- [ ] Documentation -- [ ] Tests **(only)** -- [ ] Workflow - -## Checklist - -- [Sprintlog](../SPRINTLOG.md) - - [ ] Added - - [ ] Not needed (E.g. PR contains _only_ tests) -- Rebase / Update / Merge _from_ base branch (the branch from which the current is forked) - - [ ] Done - - [ ] Not needed -- Blocking PRs - - [ ] Merged - - [ ] No blocking PRs -- PR to `master` branch - - [ ] Yes: Read [the release instructions](../docs/procedures/new_release.md) - - [ ] I have followed steps 1-7. - - [ ] No - -## Actions / Scans - - - -- **Black**: Python code formatter. Does not execute. Only tests. - Run `black .` locally to execute formatting. - - [ ] Passed -- **Pylint**: Python code linter. Does not execute. Only tests. - Fix code producing warnings. Code must get 10/10. - - [ ] Warnings fixed - - [ ] Passed -- **Prettier**: General code formatter. Our use case: MD and yaml mainly. - Run `npx prettier --write .` locally to execute formatting. - - [ ] Passed -- **Yamllint**: Linting of yaml files. - - [ ] Passed -- **Tests**: Pytest to verify that functionality works as expected. - - [ ] New tests added - - [ ] No new tests - - [ ] Passed -- **TestPyPi**: Build CLI and publish to TestPyPi in order to verify before release. - - [ ] Passed -- **CodeQL**: Scan for security vulnerabilities, bugs, errors - - [ ] New alerts: _Go through them and either fix, dismiss och ignore. Add reasoning in items below._ - - [ ] Alerts fixed: _What?_ - - [ ] Alerts ignored / dismissed: _Why?_ - - [ ] Passed -- **Trivy**: Security scanner - - [ ] New alerts: _Go through them and either fix, dismiss och ignore. Add reasoning in items below._ - - [ ] Alerts fixed: _What?_ - - [ ] Alerts ignored / dismissed: _Why?_ - - [ ] Passed -- **Snyk**: Security scanner - - [ ] New alerts: _Go through them and either fix, dismiss och ignore. Add reasoning in items below._ - - [ ] Alerts fixed: _What?_ - - [ ] Alerts ignored / dismissed: _Why?_ - - [ ] Passed + - [ ] Breaking: _Why / How? Add info here._ + - [ ] Non-breaking +- [ ] Database change: _Remember the to include a new migration version, **or** explain here why it's not needed._ +- [ ] Bug fix +- [ ] Security Alert fix +- [ ] Documentation +- [ ] Workflow +- [ ] Tests **only** + +## 4. Additional information + +- [ ] [Sprintlog](https://github.com/ScilifelabDataCentre/dds_cli/blob/dev/SPRINTLOG.md) +- [ ] Blocking PRs + - [ ] Merged +- [ ] PR to `master` branch: _If checked, read [the release instructions](https://github.com/ScilifelabDataCentre/dds_cli/blob/dev/docs/procedures/new_release.md)_ + - [ ] I have followed steps 1-8. + +## 5. Actions / Scans + +_Check the boxes when the specified checks have passed._ + +**For information on what the different checks do and how to fix it if they're failing, enter edit mode of this description or go to the [PR template](https://github.com/ScilifelabDataCentre/dds_cli/blob/dev/.github/pull_request_template.md).** + +- [ ] **Black** + +- [ ] **Pylint** + +- [ ] **Prettier** + +- [ ] **Yamllint** + +- [ ] **Tests** + +- [ ] **TestPyPI** + +- [ ] **CodeQL** + +- [ ] **Trivy** + +- [ ] **Snyk** + diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ffca02d96..3d3337510 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog ========== +.. _2.5.2: + +2.5.2 - 2023-10-25 +~~~~~~~~~~~~~~~~~~~ + +- Updated command: `dds project status delete/archive` now prints project information and asks for confirmation from user. +- "Checksum verification successful" is not printed when file integrity is verified (unless `-v` option is used); Only prints if there is an error. +- New command `dds project status extend`: Unit Admins / Personnel can extend the project deadline prior to the project expiring. + .. _2.5.1: 2.5.1 - 2023-09-25 diff --git a/SPRINTLOG.md b/SPRINTLOG.md index 7a371a90a..f9343f85d 100644 --- a/SPRINTLOG.md +++ b/SPRINTLOG.md @@ -300,4 +300,11 @@ _Nothing merged in CLI during this sprint_ # 2023-09-18 - 2023-09-29 -- GitHub Actions to generate the documentation fixed ([#1473])(https://scilifelab.atlassian.net/jira/software/projects/DDS/boards/13?selectedIssue=DDS-1473) +- GitHub Actions to generate the documentation fixed ([#650](https://github.com/ScilifelabDataCentre/dds_cli/pull/650)) +- Print project information and ask user for confirmation when deleting or archiving projects ([#655](https://github.com/ScilifelabDataCentre/dds_cli/pull/655)) + +# 2023-10-16 - 2023-10-27 + +- Change "Checksum verification successful. File integrity verified." logging level from INFO to DEBUG in order to not print for all files ([#662](https://github.com/ScilifelabDataCentre/dds_cli/pull/662) +- New command `dds project status extend` to allow extension of project deadline ([#661](https://github.com/ScilifelabDataCentre/dds_cli/pull/661) +- New version: 2.5.2 ([#660](https://github.com/ScilifelabDataCentre/dds_cli/pull/660)) diff --git a/dds_cli/__main__.py b/dds_cli/__main__.py index 13723ea87..fafc964d6 100644 --- a/dds_cli/__main__.py +++ b/dds_cli/__main__.py @@ -1196,28 +1196,22 @@ def archive_project(click_ctx, project: str, abort: bool = False): Use the `--abort` flag to indicate that something has gone wrong in the project. """ - proceed_deletion = ( - True - if click_ctx.get("NO_PROMPT", False) - else dds_cli.utils.get_deletion_confirmation(action="archive", project=project) - ) - if proceed_deletion: - try: - with dds_cli.project_status.ProjectStatusManager( - project=project, - no_prompt=click_ctx.get("NO_PROMPT", False), - token_path=click_ctx.get("TOKEN_PATH"), - ) as updater: - updater.update_status(new_status="Archived", is_aborted=abort) - except ( - dds_cli.exceptions.APIError, - dds_cli.exceptions.AuthenticationError, - dds_cli.exceptions.DDSCLIException, - dds_cli.exceptions.ApiResponseError, - dds_cli.exceptions.ApiRequestError, - ) as err: - LOG.error(err) - sys.exit(1) + try: + with dds_cli.project_status.ProjectStatusManager( + project=project, + no_prompt=click_ctx.get("NO_PROMPT", False), + token_path=click_ctx.get("TOKEN_PATH"), + ) as updater: + updater.update_status(new_status="Archived", is_aborted=abort) + except ( + dds_cli.exceptions.APIError, + dds_cli.exceptions.AuthenticationError, + dds_cli.exceptions.DDSCLIException, + dds_cli.exceptions.ApiResponseError, + dds_cli.exceptions.ApiRequestError, + ) as err: + LOG.error(err) + sys.exit(1) # -- dds project status delete -- # @@ -1231,28 +1225,56 @@ def delete_project(click_ctx, project: str): Certain meta data is kept (nothing sensitive) and it will still be listed in your projects. All data within the project is deleted. You cannot revert this change. """ - proceed_deletion = ( - True - if click_ctx.get("NO_PROMPT", False) - else dds_cli.utils.get_deletion_confirmation(action="delete", project=project) - ) - if proceed_deletion: - try: - with dds_cli.project_status.ProjectStatusManager( - project=project, - no_prompt=click_ctx.get("NO_PROMPT", False), - token_path=click_ctx.get("TOKEN_PATH"), - ) as updater: - updater.update_status(new_status="Deleted") - except ( - dds_cli.exceptions.APIError, - dds_cli.exceptions.AuthenticationError, - dds_cli.exceptions.DDSCLIException, - dds_cli.exceptions.ApiResponseError, - dds_cli.exceptions.ApiRequestError, - ) as err: - LOG.error(err) - sys.exit(1) + try: + with dds_cli.project_status.ProjectStatusManager( + project=project, + no_prompt=click_ctx.get("NO_PROMPT", False), + token_path=click_ctx.get("TOKEN_PATH"), + ) as updater: + updater.update_status(new_status="Deleted") + except ( + dds_cli.exceptions.APIError, + dds_cli.exceptions.AuthenticationError, + dds_cli.exceptions.DDSCLIException, + dds_cli.exceptions.ApiResponseError, + dds_cli.exceptions.ApiRequestError, + ) as err: + LOG.error(err) + sys.exit(1) + + +# -- dds project status extend -- # +@project_status.command(name="extend", no_args_is_help=True) +# Options +@project_option(required=True) +@click.option( + "--new-deadline", + required=False, + type=int, + help="Number of days to extend the deadline.", +) +@click.pass_obj +def extend_deadline(click_ctx, project: str, new_deadline: int): + """Extend a project deadline by an specified number of days. + + It consumes one of allowed times to renew data access. + """ + try: + with dds_cli.project_status.ProjectStatusManager( + project=project, + no_prompt=click_ctx.get("NO_PROMPT", False), + token_path=click_ctx.get("TOKEN_PATH"), + ) as updater: + updater.extend_deadline(new_deadline=new_deadline) + except ( + dds_cli.exceptions.APIError, + dds_cli.exceptions.AuthenticationError, + dds_cli.exceptions.DDSCLIException, + dds_cli.exceptions.ApiResponseError, + dds_cli.exceptions.ApiRequestError, + ) as err: + LOG.error(err) + sys.exit(1) # -- dds project status busy -- # diff --git a/dds_cli/base.py b/dds_cli/base.py index febc5c393..0261f9f8a 100644 --- a/dds_cli/base.py +++ b/dds_cli/base.py @@ -133,6 +133,43 @@ def __exit__(self, exception_type, exception_value, traceback, max_fileerrs: int return True + # Public methods ############################### Public methods # + + def get_project_info(self): + """Collect project information from API.""" + + # Get info about a project from API + response, _ = dds_cli.utils.perform_request( + DDSEndpoint.PROJ_INFO, + method="get", + headers=self.token, + params={"project": self.project}, + error_message="Failed to get project information", + ) + + project_info = response.get("project_info") + + # If not project info was retrieved from the request, throw an exception + if not project_info: + raise dds_cli.exceptions.ApiResponseError(message="No project information to display.") + + return project_info + + def generate_project_table(self, project_info): + """Generate a table from some project info provided""" + + # Print project info table + table = dds_cli.utils.create_table( + title="Project information.", + columns=["Project ID", "Created by", "Status", "Last updated", "Size"], + rows=[ + project_info, + ], + caption=f"Information about project {project_info['Project ID']}", + ) + + return table + # Private methods ############################### Private methods # def __get_safespring_keys(self): """Get safespring keys.""" diff --git a/dds_cli/file_encryptor.py b/dds_cli/file_encryptor.py index c0d4852f1..97a5aea6e 100644 --- a/dds_cli/file_encryptor.py +++ b/dds_cli/file_encryptor.py @@ -132,7 +132,7 @@ def verify_checksum(file: pathlib.Path, correct_checksum): else: if checksum.hexdigest() == correct_checksum: verified, error = (True, "File integrity verified.") - LOG.info("Checksum verification successful. File integrity verified.") + LOG.debug("Checksum verification successful. File integrity verified.") else: error = "Checksum verification failed. File compromised." LOG.warning(error) diff --git a/dds_cli/project_info.py b/dds_cli/project_info.py index cfa393dc4..91b5cdb71 100644 --- a/dds_cli/project_info.py +++ b/dds_cli/project_info.py @@ -44,37 +44,13 @@ def __init__( self.project = project # Public methods ###################### Public methods # - def get_project_info(self): - """Collect project information from API.""" - # Get info about a project from API - response, _ = dds_cli.utils.perform_request( - DDSEndpoint.PROJ_INFO, - method="get", - headers=self.token, - params={"project": self.project}, - error_message="Failed to get project information", - ) - - project_info = response.get("project_info") - if not project_info: - raise dds_cli.exceptions.ApiResponseError(message="No project information to display.") - - return project_info def show_project_info(self): """Display info about a project.""" - # Get project info from API - project_info = self.get_project_info() - # Print project info table - table = dds_cli.utils.create_table( - title="Project information.", - columns=["Project ID", "Created by", "Status", "Last updated", "Size"], - rows=[ - project_info, - ], - caption=f"Information about project {project_info['Project ID']}", - ) + # Get project info from api + project_info = self.get_project_info() + table = self.generate_project_table(project_info=project_info) dds_cli.utils.console.print(table) # Print Title and Description below the table diff --git a/dds_cli/project_status.py b/dds_cli/project_status.py index d1c6a2e36..253a61e25 100644 --- a/dds_cli/project_status.py +++ b/dds_cli/project_status.py @@ -2,10 +2,13 @@ import datetime import logging import typing +import sys +from dateutil.parser import parse # Installed import pytz import tzlocal +import rich # Own modules from dds_cli import base @@ -31,12 +34,14 @@ class ProjectStatusManager(base.DDSBaseClass): def __init__( self, project: str, + authenticate: bool = True, no_prompt: bool = False, token_path: str = None, ): """Handle actions regarding project status in the cli.""" # Initiate DDSBaseClass to authenticate user super().__init__( + authenticate=authenticate, no_prompt=no_prompt, method_check=False, token_path=token_path, @@ -44,6 +49,7 @@ def __init__( self.project = project # Public methods ###################### Public methods # + def get_status(self, show_history): """Get current status and status history of the project.""" resp_json, _ = dds_cli.utils.perform_request( @@ -102,6 +108,36 @@ def update_status(self, new_status, deadline=None, is_aborted=False, no_mail=Fal if is_aborted: extra_params["is_aborted"] = is_aborted + # If the status is going to be archived or deleted. Ask for confirmation + if new_status in ["Archived", "Deleted"]: + # get project info + try: + project_info = self.get_project_info() + except exceptions.ApiResponseError: + dds_cli.utils.console.print( + "No project information could be displayed at this moment!" + ) + else: + table = self.generate_project_table(project_info=project_info) + dds_cli.utils.console.print(table) + + # Create confirmation prompt + print_info = ( + f"Are you sure you want to modify the status of {self.project}? All its contents " + ) + if new_status == "Deleted": + print_info += "and metainfo " + print_info += ( + "will be deleted!\n" + f"The project '{self.project}' is about to be [b][blue]{new_status}[/blue][/b].\n" + ) + + dds_cli.utils.console.print(print_info) + + if not rich.prompt.Confirm.ask("-"): + LOG.info("Probably for the best. Exiting.") + sys.exit(0) + response_json, _ = dds_cli.utils.perform_request( endpoint=DDSEndpoint.UPDATE_PROJ_STATUS, headers=self.token, @@ -113,6 +149,117 @@ def update_status(self, new_status, deadline=None, is_aborted=False, no_mail=Fal dds_cli.utils.console.print(f"Project {response_json.get('message')}") + def extend_deadline(self, new_deadline=None): + """Extend the project deadline.""" + # Define initial parameters + extra_params = {"send_email": False} + + # Fetch project status and default deadline + response_json, _ = dds_cli.utils.perform_request( + endpoint=DDSEndpoint.UPDATE_PROJ_STATUS, + headers=self.token, + method="patch", + params={"project": self.project}, + json=extra_params, + error_message="Failed to extend project deadline", + ) + + # Structure of the response: + # { + # 'default_unit_days': 30, + # 'project_info': { + # 'Created by': 'First Unit User', + # 'Description': 'This is a test project', + # 'Last updated': 'Wed, 18 Oct 2023 08:40:43 GMT', + # 'PI': 'support@example.com', + # 'Project ID': 'project_1', + # 'Size': 0, + # 'Status': 'Available', + # 'Title': 'First Project' + # }, + # 'project_status': { + # 'current_deadline': 'Sat, 04 Nov 2023 23:59:59 GMT', + # 'current_status': 'Available'}, + # } + + # Check that the returned information was ok + keys = ["project_info", "project_status", "default_unit_days"] + ( + project_info, + project_status, + default_unit_days, + *_, + ) = dds_cli.utils.get_required_in_response(keys=keys, response=response_json) + + # Check and extract the required information for the operation + current_status, *_ = dds_cli.utils.get_required_in_response( + keys=["current_status"], response=project_status + ) + + # if the project is still in progress it won't have a current_deadline parameter + if not current_status == "Available": + raise exceptions.DDSCLIException( + "You can only extend the deadline for a project that has the status 'Available'." + ) + + current_deadline, *_ = dds_cli.utils.get_required_in_response( + keys=["current_deadline"], response=project_status + ) + project_id, *_ = dds_cli.utils.get_required_in_response( + keys=["Project ID"], response=project_info + ) + + # print information about the project status and table with the project info + print_info = ( + f"\nCurrent deadline: [b][green]{current_deadline}[/green][/b]\n" + f"Default deadline extension: [b][green]{default_unit_days}[/green][/b] days\n" + ) + table = self.generate_project_table(project_info=project_info) + dds_cli.utils.console.print(table) + dds_cli.utils.console.print(print_info) + + # If it wasnt provided during the command click, ask the user for the new deadline + if not new_deadline: + # Question number of days to extend the deadline + prompt_question = ( + "How many days would you like to extend the project deadline with? " + "Leave empty in order to choose the default" + ) + new_deadline = rich.prompt.IntPrompt.ask(prompt_question, default=default_unit_days) + + # Confirm operation question + new_deadline_date = parse(current_deadline) + datetime.timedelta(days=new_deadline) + new_deadline_date = new_deadline_date.strftime("%a,%d %b %Y %H:%M:%S") + prompt_question = ( + f"\nThe new deadline for project {project_id} will be: [b][blue]{new_deadline_date}[/b][/blue]" + "\n\n[b][blue]Are you sure [/b][/blue]you want to perform this operation? " + "\nYou can only extend the data availability a maximum of " + "[b][blue]3 times[/b][/blue], this consumes one of those times." + ) + + if not rich.prompt.Confirm.ask(prompt_question): + LOG.info("Probably for the best. Exiting.") + sys.exit(0) + + # Update parameters for the second request + extra_params = {**extra_params, "confirmed": True, "new_deadline_in": new_deadline} + + response_json, _ = dds_cli.utils.perform_request( + endpoint=DDSEndpoint.UPDATE_PROJ_STATUS, + headers=self.token, + method="patch", + params={"project": self.project}, + json=extra_params, + error_message="Failed to extend project deadline", + ) + message = response_json.get("message") + if not message: + raise exceptions.DDSCLIException( + "No message returned from API. Cannot verify extension of project deadline." + ) + + LOG.info(message) + class ProjectBusyStatusManager(base.DDSBaseClass): """Project Busy Status manager class.""" diff --git a/dds_cli/utils.py b/dds_cli/utils.py index c04e487d2..1b0045378 100644 --- a/dds_cli/utils.py +++ b/dds_cli/utils.py @@ -165,6 +165,8 @@ def perform_request( request_method = requests.post elif method == "delete": request_method = requests.delete + elif method == "patch": + request_method = requests.patch def transform_paths(json_input): """Make paths serializable.""" diff --git a/dds_cli/version.py b/dds_cli/version.py index 0fb89294d..fdda3a943 100644 --- a/dds_cli/version.py +++ b/dds_cli/version.py @@ -1,3 +1,3 @@ """DDS CLI version.""" -__version__ = "2.5.1" +__version__ = "2.5.2" diff --git a/docs/procedures/new_release.md b/docs/procedures/new_release.md index c9f2a154b..430ee4046 100644 --- a/docs/procedures/new_release.md +++ b/docs/procedures/new_release.md @@ -1,24 +1,27 @@ # How to create a new release -1. Fork a new branch from `dev` -2. Update the version [changelog](../../CHANGELOG.rst) +1. Create a PR from `dev` to `master`: "New release". Use this for step 3. +2. Fork a new branch from `dev`: "New version & changelog" +3. Update the version [changelog](../../CHANGELOG.rst) located at `dds_cli/CHANGELOG.rst` + + **Tip:** Use the PR to `master` (step 1) to see all changes since last release. - The new version should be at the top of the page - List the changes that the users will / may notice - Do not add information regarding workflow (e.g. GitHub Actions) etc -3. Update the version in [`version.py`](../../dds_cli/version.py) +4. Update the version in [`version.py`](../../dds_cli/version.py) - _Minor changes, e.g. bug fix_: Minor version upgrade, e.g. `1.0.1 --> 1.0.2` - _Small changes, e.g. new feature_: Mid version upgrade, e.g. `1.1.0 --> 1.2.0` - - _Breaking changes or large new feature(s)_: Major version upgrade, e.g. `1.0.0 --> 2.0.0` + - _Breaking changes or large new feature(s)_: Major version upgrade, e.g. `1.0.0 --> 2.0.0` _SHOULD NEVER BE DONE UNLESS THE API ALSO HAS THIS IDENTICAL CHANGE._ > Will break if Web / API version not bumped as well -4. Push changelog and version change to branch -5. Run the `rich-codex` action [here](https://github.com/ScilifelabDataCentre/dds_cli/actions/workflows/rich-codex-cli.yml); Choose your current branch where it says "Run workflow" +5. Push changelog and version change to branch +6. Run the `rich-codex` action [here](https://github.com/ScilifelabDataCentre/dds_cli/actions/workflows/rich-codex-cli.yml); Choose your current branch where it says "Run workflow" - `rich-codex` will push changes to your branch; these commits _will not be signed_ - - In order for you to merge these changes into the `dev`/`master` branch, all commits need to be signed: + - In order for you to merge these changes into the `dev` branch, all commits need to be signed: 1. Pull the changes to your local branch 2. Run the following command: ```bash @@ -29,10 +32,10 @@ ```bash git push --force ``` -6. Create a new PR from `` to `dev` +7. Create a new PR from `` to `dev` 1. Verify that the new code example images look ok 2. Wait for approval and merge by Product Owner or admin -7. Create a PR from `dev` to `master` +8. Create a PR from `dev` to `master` - Are you bumping the major version (e.g. 1.x.x to 2.x.x)? - Yes: Add this info to the PR. @@ -48,7 +51,7 @@ > Documentation changes are automatically updated on GitHub pages when there's a push to `master`. However, in order to keep things consistent and to avoid confusion with the versions, always release a new version when changes are pushed to `master` (assuming all the changes have been verified) -8. [Draft a new release](https://github.com/ScilifelabDataCentre/dds_cli/releases) +9. [Draft a new release](https://github.com/ScilifelabDataCentre/dds_cli/releases) 1. `Choose a tag` → `Find or create a new tag` → Fill in the new version, e.g. if the new version is `1.0.0`, you should fill in `v1.0.0`. 2. `Target` should be set to `master` @@ -61,25 +64,25 @@ A new version of the CLI will be published to [PyPi](https://pypi.org/project/dds-cli/) -9. Verify that the new CLI version is updated on Uppmax +10. Verify that the new CLI version is updated on Uppmax - Uppmax automatically upgrades the `dds-cli` version every day at midnight. Double-check that this has worked, if you have an Uppmax account. + Uppmax automatically upgrades the `dds-cli` version every day at midnight. Double-check that this has worked, if you have an Uppmax account. - If there has been a major version change though and the CLI contains breaking changes, _Uppmax should be notified well in advance_ in order to plan for an upgrade at a specific time so that the users are blocked (automatic functionality in dds_web) for as short time as possible. + If there has been a major version change though and the CLI contains breaking changes, _Uppmax should be notified well in advance_ in order to plan for an upgrade at a specific time so that the users are blocked (automatic functionality in dds_web) for as short time as possible. - ``` - [Recipient]: support@uppmax.uu.se - [Subject]: (Pavlin Mitev) Upgrade dds-cli module + ``` + [Recipient]: support@uppmax.uu.se + [Subject]: (Pavlin Mitev) Upgrade dds-cli module - [Message]: - Hi, + [Message]: + Hi, - We will be releasing a new major version of the dds-cli on . The changes are breaking - would it be possible for you do a manual version upgrade at that time, so that the users don't experience issues? + We will be releasing a new major version of the dds-cli on . The changes are breaking - would it be possible for you do a manual version upgrade at that time, so that the users don't experience issues? - Thank you in advance! - ``` + Thank you in advance! + ``` -10. Inform users that there is a new version by adding a Message of the Day: `dds motd add` +11. Inform users that there is a new version by adding a Message of the Day: `dds motd add` - If users do not upgrade the CLI when there is a new version, they may experience issues and errors. - If there is a major version mismatch between the API and CLI (e.g. API version 1.0.0 and CLI version 2.0.0 or vice versa), the DDS will inform the users that they are blocked from using the DDS until they have upgraded. diff --git a/img/dds-help-1.svg b/img/dds-help-1.svg index 123590349..29e92e594 100644 --- a/img/dds-help-1.svg +++ b/img/dds-help-1.svg @@ -19,140 +19,140 @@ font-weight: 700; } - .terminal-2708983732-matrix { + .terminal-3455438773-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2708983732-title { + .terminal-3455438773-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2708983732-r1 { fill: #c5c8c6 } -.terminal-2708983732-r2 { fill: #868887;font-style: italic; } -.terminal-2708983732-r3 { fill: #c5c8c6;font-weight: bold } -.terminal-2708983732-r4 { fill: #d0b344;font-weight: bold } -.terminal-2708983732-r5 { fill: #68a0b3;font-weight: bold } -.terminal-2708983732-r6 { fill: #868887 } -.terminal-2708983732-r7 { fill: #4e707b;font-weight: bold } -.terminal-2708983732-r8 { fill: #98a84b;font-weight: bold } -.terminal-2708983732-r9 { fill: #8d7b39;font-weight: bold } + .terminal-3455438773-r1 { fill: #c5c8c6 } +.terminal-3455438773-r2 { fill: #868887;font-style: italic; } +.terminal-3455438773-r3 { fill: #c5c8c6;font-weight: bold } +.terminal-3455438773-r4 { fill: #d0b344;font-weight: bold } +.terminal-3455438773-r5 { fill: #68a0b3;font-weight: bold } +.terminal-3455438773-r6 { fill: #868887 } +.terminal-3455438773-r7 { fill: #4e707b;font-weight: bold } +.terminal-3455438773-r8 { fill: #98a84b;font-weight: bold } +.terminal-3455438773-r9 { fill: #8d7b39;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -164,46 +164,46 @@ - + - - $ dds -     ︵  - ︵ (  )   ︵  -(  ) ) (  (  )   SciLifeLab Data Delivery System  - ︶  (  ) ) (    https://delivery.scilifelab.se/  -      ︶ (  )    CLI Version 2.5.1  -          ︶ -[..truncated..] -Usage: dds [OPTIONSCOMMAND [ARGS]... - - SciLifeLab Data Delivery System (DDS) command line interface.                                       -Access token is saved in a .dds_cli_token file in the home directory. -The token is valid for 7 days. Make sure your token is valid long enough for the delivery to  -finish. To avoid that a delivery fails because of an expired token, we recommend reauthenticating  -yourself before each delivery ('dds data put' / 'get'). - -╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮ ---verbose-v  Print verbose output to the console.                               ---log-file-l<filename>  Save a log to a file.                                              ---no-prompt  Run without any interactive features.                              ---token-path-tpTEXT        The path where the authentication token will be stored. For a      -                                normal use-case, this should not be needed.                        ---version  Display the version of this software.                              ---help  List the options of any DDS subcommand and its default settings.   -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮ -auth         Group command for creating and managing authenticated sessions (valid for 7 days).  -data         Group command for uploading, downloading and managing project data.                 -ls           List the active projects you have access to or the project contents.                -maintenance  [Super Admins only] Activate / Deactivate / Display status for Maintenance mode.    -motd         [Super Admins only] Group command for managing Message of the Day within DDS.       -project      Group command for creating and managing projects within the DDS.                    -stats        Get statistics in the DDS.                                                          -unit         [Super Admins only] Group command for managing units.                               -user         Group command for managing user accounts, including your own.                       -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ - + + $ dds +     ︵  + ︵ (  )   ︵  +(  ) ) (  (  )   SciLifeLab Data Delivery System  + ︶  (  ) ) (    https://delivery.scilifelab.se/  +      ︶ (  )    CLI Version 2.5.2  +          ︶ +[..truncated..] +Usage: dds [OPTIONSCOMMAND [ARGS]... + + SciLifeLab Data Delivery System (DDS) command line interface.                                       +Access token is saved in a .dds_cli_token file in the home directory. +The token is valid for 7 days. Make sure your token is valid long enough for the delivery to  +finish. To avoid that a delivery fails because of an expired token, we recommend reauthenticating  +yourself before each delivery ('dds data put' / 'get'). + +╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮ +--verbose-v  Print verbose output to the console.                               +--log-file-l<filename>  Save a log to a file.                                              +--no-prompt  Run without any interactive features.                              +--token-path-tpTEXT        The path where the authentication token will be stored. For a      +                                normal use-case, this should not be needed.                        +--version  Display the version of this software.                              +--help  List the options of any DDS subcommand and its default settings.   +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮ +auth         Group command for creating and managing authenticated sessions (valid for 7 days).  +data         Group command for uploading, downloading and managing project data.                 +ls           List the active projects you have access to or the project contents.                +maintenance  [Super Admins only] Activate / Deactivate / Display status for Maintenance mode.    +motd         [Super Admins only] Group command for managing Message of the Day within DDS.       +project      Group command for creating and managing projects within the DDS.                    +stats        Get statistics in the DDS.                                                          +unit         [Super Admins only] Group command for managing units.                               +user         Group command for managing user accounts, including your own.                       +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + diff --git a/img/dds-help-2.svg b/img/dds-help-2.svg index 40f1a2944..d33730a7c 100644 --- a/img/dds-help-2.svg +++ b/img/dds-help-2.svg @@ -19,140 +19,140 @@ font-weight: 700; } - .terminal-835047895-matrix { + .terminal-1581502936-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-835047895-title { + .terminal-1581502936-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-835047895-r1 { fill: #c5c8c6 } -.terminal-835047895-r2 { fill: #868887;font-style: italic; } -.terminal-835047895-r3 { fill: #c5c8c6;font-weight: bold } -.terminal-835047895-r4 { fill: #d0b344;font-weight: bold } -.terminal-835047895-r5 { fill: #68a0b3;font-weight: bold } -.terminal-835047895-r6 { fill: #868887 } -.terminal-835047895-r7 { fill: #4e707b;font-weight: bold } -.terminal-835047895-r8 { fill: #98a84b;font-weight: bold } -.terminal-835047895-r9 { fill: #8d7b39;font-weight: bold } + .terminal-1581502936-r1 { fill: #c5c8c6 } +.terminal-1581502936-r2 { fill: #868887;font-style: italic; } +.terminal-1581502936-r3 { fill: #c5c8c6;font-weight: bold } +.terminal-1581502936-r4 { fill: #d0b344;font-weight: bold } +.terminal-1581502936-r5 { fill: #68a0b3;font-weight: bold } +.terminal-1581502936-r6 { fill: #868887 } +.terminal-1581502936-r7 { fill: #4e707b;font-weight: bold } +.terminal-1581502936-r8 { fill: #98a84b;font-weight: bold } +.terminal-1581502936-r9 { fill: #8d7b39;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -164,46 +164,46 @@ - + - - $ dds --help -     ︵  - ︵ (  )   ︵  -(  ) ) (  (  )   SciLifeLab Data Delivery System  - ︶  (  ) ) (    https://delivery.scilifelab.se/  -      ︶ (  )    CLI Version 2.5.1  -          ︶ -[..truncated..] -Usage: dds [OPTIONSCOMMAND [ARGS]... - - SciLifeLab Data Delivery System (DDS) command line interface.                                       -Access token is saved in a .dds_cli_token file in the home directory. -The token is valid for 7 days. Make sure your token is valid long enough for the delivery to  -finish. To avoid that a delivery fails because of an expired token, we recommend reauthenticating  -yourself before each delivery ('dds data put' / 'get'). - -╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮ ---verbose-v  Print verbose output to the console.                               ---log-file-l<filename>  Save a log to a file.                                              ---no-prompt  Run without any interactive features.                              ---token-path-tpTEXT        The path where the authentication token will be stored. For a      -                                normal use-case, this should not be needed.                        ---version  Display the version of this software.                              ---help  List the options of any DDS subcommand and its default settings.   -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮ -auth         Group command for creating and managing authenticated sessions (valid for 7 days).  -data         Group command for uploading, downloading and managing project data.                 -ls           List the active projects you have access to or the project contents.                -maintenance  [Super Admins only] Activate / Deactivate / Display status for Maintenance mode.    -motd         [Super Admins only] Group command for managing Message of the Day within DDS.       -project      Group command for creating and managing projects within the DDS.                    -stats        Get statistics in the DDS.                                                          -unit         [Super Admins only] Group command for managing units.                               -user         Group command for managing user accounts, including your own.                       -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ - + + $ dds --help +     ︵  + ︵ (  )   ︵  +(  ) ) (  (  )   SciLifeLab Data Delivery System  + ︶  (  ) ) (    https://delivery.scilifelab.se/  +      ︶ (  )    CLI Version 2.5.2  +          ︶ +[..truncated..] +Usage: dds [OPTIONSCOMMAND [ARGS]... + + SciLifeLab Data Delivery System (DDS) command line interface.                                       +Access token is saved in a .dds_cli_token file in the home directory. +The token is valid for 7 days. Make sure your token is valid long enough for the delivery to  +finish. To avoid that a delivery fails because of an expired token, we recommend reauthenticating  +yourself before each delivery ('dds data put' / 'get'). + +╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮ +--verbose-v  Print verbose output to the console.                               +--log-file-l<filename>  Save a log to a file.                                              +--no-prompt  Run without any interactive features.                              +--token-path-tpTEXT        The path where the authentication token will be stored. For a      +                                normal use-case, this should not be needed.                        +--version  Display the version of this software.                              +--help  List the options of any DDS subcommand and its default settings.   +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮ +auth         Group command for creating and managing authenticated sessions (valid for 7 days).  +data         Group command for uploading, downloading and managing project data.                 +ls           List the active projects you have access to or the project contents.                +maintenance  [Super Admins only] Activate / Deactivate / Display status for Maintenance mode.    +motd         [Super Admins only] Group command for managing Message of the Day within DDS.       +project      Group command for creating and managing projects within the DDS.                    +stats        Get statistics in the DDS.                                                          +unit         [Super Admins only] Group command for managing units.                               +user         Group command for managing user accounts, including your own.                       +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + diff --git a/img/dds-version.svg b/img/dds-version.svg index c2c3c5dc4..f185cd021 100644 --- a/img/dds-version.svg +++ b/img/dds-version.svg @@ -19,31 +19,31 @@ font-weight: 700; } - .terminal-1539323793-matrix { + .terminal-1540372370-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1539323793-title { + .terminal-1540372370-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1539323793-r1 { fill: #c5c8c6 } -.terminal-1539323793-r2 { fill: #868887;font-style: italic; } + .terminal-1540372370-r1 { fill: #c5c8c6 } +.terminal-1540372370-r2 { fill: #868887;font-style: italic; } - + - + - + @@ -55,12 +55,12 @@ - + - - $ dds --version -[..truncated..] -Data Delivery System, version 2.5.1 + + $ dds --version +[..truncated..] +Data Delivery System, version 2.5.2 diff --git a/tests/test_project_status.py b/tests/test_project_status.py new file mode 100644 index 000000000..46dead023 --- /dev/null +++ b/tests/test_project_status.py @@ -0,0 +1,490 @@ +import pytest +from requests_mock.mocker import Mocker +import unittest +from dds_cli import DDSEndpoint +from dds_cli import project_status +from _pytest.logging import LogCaptureFixture +from _pytest.capture import CaptureFixture +import logging +from dds_cli.exceptions import ApiResponseError, DDSCLIException +import datetime + +import typing + +# init + +######### + +project_name = "Test" +returned_response_get_info: typing.Dict = { + "Project ID": "Test001", + "Created by": "Mr Bean", + "Status": "In progress", + "Last updated": "None", + "Size": "0.0 B", + "Title": "Test", + "Description": "a description", + "PI": "pi@a.se", +} +returned_response_archived_ok: typing.Dict = { + "message": f"{project_name} updated to status Archived. An e-mail notification has been sent." +} +returned_response_deleted_ok: typing.Dict = { + "message": f"{project_name} updated to status Deleted. An e-mail notification has been sent." +} +returned_response_available_ok: typing.Dict = { + "message": f"{project_name} updated to status Available. An e-mail notification has been sent." +} + + +deadline = str(datetime.datetime.now() + datetime.timedelta(days=1)) +default_unit_days = 30 +returned_response_extend_deadline_fetch_information = { + "project_info": returned_response_get_info, + "default_unit_days": default_unit_days, + "warning": "Operation must be confirmed before proceding.", + "project_status": {"current_deadline": deadline, "current_status": "Available"}, +} +returned_response_extend_deadline_fetch_information_in_progress = { + **returned_response_extend_deadline_fetch_information, + "project_status": {"current_status": "In progress"}, +} + +returned_response_extend_deadline_ok: typing.Dict = { + "message": f"Project {project_name} has been given a new deadline." +} + + +######### +def check_table_proj_info(table_output): + assert "┏━━━━━" in table_output.out # A table has generated + assert f"{returned_response_get_info['Project ID']}" in table_output.out + assert f"{returned_response_get_info['Created by']}" in table_output.out + assert f"{returned_response_get_info['Status']}" in table_output.out + assert f"{returned_response_get_info['Last updated']}" in table_output.out + assert f"{returned_response_get_info['Size']}" in table_output.out + + +def perform_archive_delete_operation(new_status, confirmed, mock, json_project_info=None): + returned_response: typing.Dict = { + "message": f"{project_name} updated to status {new_status}. An e-mail notification has been sent." + } + + if not json_project_info: + json_project_info = {"project_info": returned_response_get_info} + + # Create mocked request - real request not executed + mock.get( + DDSEndpoint.PROJ_INFO, + status_code=200, + json=json_project_info, + ) + mock.post(DDSEndpoint.UPDATE_PROJ_STATUS, status_code=200, json=returned_response) + + if not confirmed: + # capture system exit on not accepting operation + with pytest.raises(SystemExit): + with project_status.ProjectStatusManager( + project=project_name, no_prompt=True, authenticate=False + ) as status_mngr: + status_mngr.token = {} # required, otherwise none + status_mngr.update_status(new_status=new_status) + else: + with project_status.ProjectStatusManager( + project=project_name, no_prompt=True, authenticate=False + ) as status_mngr: + status_mngr.token = {} # required, otherwise none + status_mngr.update_status(new_status=new_status) + + +def check_output_project_info(new_status, captured_output, caplog_tuples=None): + # Becuase of the bold and coloring formating, it is better to test for this keyworkd. Insetad of trying to find + # the whole string The project 'project_1' is about to be Deleted. + assert f"{project_name}" in captured_output.out + assert f"{new_status}" + + check_table_proj_info(table_output=captured_output) + # if not confirmed operation + if caplog_tuples: + assert ( + "dds_cli.project_status", + logging.INFO, + "Probably for the best. Exiting.", + ) in caplog_tuples + + +def check_output_extend_deadline(captured_output, caplog_tuples=None): + assert "Current deadline:" in captured_output.out + assert "Default deadline extension:" in captured_output.out + + check_table_proj_info(table_output=captured_output) + + # if not confirmed operation + if caplog_tuples: + assert ( + "dds_cli.project_status", + logging.INFO, + "Probably for the best. Exiting.", + ) in caplog_tuples + + +# tests + + +def test_init_project_status_manager(): + """Create manager.""" + status_mngr: project_status.ProjectStatusManager = project_status.ProjectStatusManager( + project=project_name, no_prompt=True, authenticate=False + ) + assert isinstance(status_mngr, project_status.ProjectStatusManager) + + +def test_fail_update_project(capsys: CaptureFixture): + """Test that fails when trying to update the project status""" + + # Create mocker + with Mocker() as mock: + # Create mocked request - real request not executed + mock.get(DDSEndpoint.PROJ_INFO, status_code=200, json={}) + mock.post(DDSEndpoint.UPDATE_PROJ_STATUS, status_code=403, json={}) + + with pytest.raises(DDSCLIException) as err: + with project_status.ProjectStatusManager( + project=project_name, no_prompt=True, authenticate=False + ) as status_mngr: + status_mngr.token = {} # required, otherwise none + status_mngr.update_status(new_status="Available") + + assert "Failed to update project status" in str(err.value) + + +def test_fail_display_project_info(capsys: CaptureFixture): + """Test that fails when trying to retrieve the project info to display""" + + # Create mocker + with Mocker() as mock: + # Create mocked request - real request not executed + mock.get(DDSEndpoint.PROJ_INFO, status_code=403, json={}) + mock.post(DDSEndpoint.UPDATE_PROJ_STATUS, status_code=200, json={}) + + with pytest.raises(DDSCLIException) as err: + with project_status.ProjectStatusManager( + project=project_name, no_prompt=True, authenticate=False + ) as status_mngr: + status_mngr.token = {} # required, otherwise none + status_mngr.update_status(new_status="Archived") + + assert "Failed to get project information:" in str(err.value) + + +def test_release_project(capsys: CaptureFixture): + """Test that tries to release a project and seeting up as available""" + + # Create mocker + with Mocker() as mock: + # Create mocked request - real request not executed + mock.get(DDSEndpoint.PROJ_INFO, status_code=200, json={}) + mock.post( + DDSEndpoint.UPDATE_PROJ_STATUS, + status_code=200, + json=returned_response_available_ok, + ) + + with project_status.ProjectStatusManager( + project=project_name, no_prompt=True, authenticate=False + ) as status_mngr: + status_mngr.token = {} # required, otherwise none + status_mngr.update_status(new_status="Available") + + assert returned_response_available_ok["message"] in capsys.readouterr().out + + +def test_delete_project_no(capsys: CaptureFixture, monkeypatch, caplog: LogCaptureFixture): + """Test that tries to delete a project, but the user selects no to perfrom the operation""" + + confirmed = False + caplog.set_level(logging.INFO) + # Create mocker + with Mocker() as mock: + # set confirmation object to false + monkeypatch.setattr("rich.prompt.Confirm.ask", lambda question: confirmed) + perform_archive_delete_operation(new_status="Deleted", confirmed=confirmed, mock=mock) + captured_output = capsys.readouterr() + + # for some reason the captured log includees line break here. But in the client it displays normal -> + # could be because of the if-else to build this log + assert ( + f"Are you sure you want to modify the status of {project_name}? All its contents and \nmetainfo will be deleted!" + in captured_output.out + ) + + # check the rest of the project info is displayed correctly + check_output_project_info( + new_status="Deleted", + captured_output=captured_output, + caplog_tuples=caplog.record_tuples, + ) + + +def test_archive_project_no(capsys: CaptureFixture, monkeypatch, caplog: LogCaptureFixture): + """Test that tries to archive a project, but the user selects no to perfrom the operation""" + + confirmed = False + caplog.set_level(logging.INFO) + # Create mocker + with Mocker() as mock: + # set confirmation object to false + monkeypatch.setattr("rich.prompt.Confirm.ask", lambda question: confirmed) + perform_archive_delete_operation(new_status="Archived", confirmed=confirmed, mock=mock) + captured_output = capsys.readouterr() + + # for some reason the captured log includees line break here. But in the client it displays normal -> + # could be because of the if-else to build this log + assert ( + f"Are you sure you want to modify the status of {project_name}? All its contents will be \ndeleted!" + in captured_output.out + ) + + # check the rest of the project info is displayed correctly + check_output_project_info( + new_status="Archived", + captured_output=captured_output, + caplog_tuples=caplog.record_tuples, + ) + + +def test_delete_project_yes(capsys: CaptureFixture, monkeypatch, caplog: LogCaptureFixture): + """Test that tries to delete a project, the user accepts the operation""" + + confirmed = True + # Create mocker + with Mocker() as mock: + # set confirmation object to true + monkeypatch.setattr("rich.prompt.Confirm.ask", lambda question: confirmed) + perform_archive_delete_operation(new_status="Deleted", confirmed=confirmed, mock=mock) + captured_output = capsys.readouterr() + + assert returned_response_deleted_ok["message"] in captured_output.out + # check the rest of the project info is displayed correctly + check_output_project_info( + new_status="Deleted", captured_output=captured_output, caplog_tuples=None + ) + + +def test_archive_project_yes(capsys: CaptureFixture, monkeypatch, caplog: LogCaptureFixture): + """Test that tries to archive a project, the user accepts the operation""" + + confirmed = True + # Create mocker + with Mocker() as mock: + # set confirmation object to true + monkeypatch.setattr("rich.prompt.Confirm.ask", lambda question: confirmed) + perform_archive_delete_operation(new_status="Archived", confirmed=confirmed, mock=mock) + captured_output = capsys.readouterr() + + assert returned_response_archived_ok["message"] in captured_output.out + check_output_project_info( + new_status="Archived", captured_output=captured_output, caplog_tuples=None + ) + + +def test_no_project_info(capsys: CaptureFixture, monkeypatch): + """Test that check when it returns no project info during request""" + + confirmed = True + # Create mocker + with Mocker() as mock: + # set confirmation object to True + monkeypatch.setattr("rich.prompt.Confirm.ask", lambda question: True) + + perform_archive_delete_operation( + new_status="Archived", + confirmed=confirmed, + mock=mock, + json_project_info={"project_info": {}}, + ) + assert ( + "No project information could be displayed at this moment!" in capsys.readouterr().out + ) + + +def test_update_extra_params(capsys: CaptureFixture, monkeypatch, caplog: LogCaptureFixture): + """Test that update the project status providing extra params""" + + # Create mocker + with Mocker() as mock: + # Create mocked request - real request not executed + mock.get( + DDSEndpoint.PROJ_INFO, + status_code=200, + json={"project_info": returned_response_get_info}, + ) + mock.post( + DDSEndpoint.UPDATE_PROJ_STATUS, + status_code=200, + json=returned_response_archived_ok, + ) + monkeypatch.setattr("rich.prompt.Confirm.ask", lambda question: True) + + with project_status.ProjectStatusManager( + project=project_name, no_prompt=True, authenticate=False + ) as status_mngr: + status_mngr.token = {} # required, otherwise none + status_mngr.update_status(new_status="Archived", is_aborted=True, deadline=10) + + assert returned_response_archived_ok["message"] in capsys.readouterr().out + + +def test_extend_deadline_no_confirmed( + capsys: CaptureFixture, monkeypatch, caplog: LogCaptureFixture +): + """The user decided to not accept the extension""" + + confirmed = False + caplog.set_level(logging.INFO) + + # Create mocker + with Mocker() as mock: + # set confirmation object to false + monkeypatch.setattr("rich.prompt.Confirm.ask", lambda question: confirmed) + # Set number of days to extend deadline - Bc of the default value in the function we need to create a mock answer + with unittest.mock.patch("rich.prompt.IntPrompt.ask") as deadline: + deadline.return_value = 2 + + # Create first mocked request - not confirmed + mock.patch( + DDSEndpoint.UPDATE_PROJ_STATUS, + status_code=200, + json=returned_response_extend_deadline_fetch_information, + ) + + # capture system exit on not accepting operation + with pytest.raises(SystemExit): + with project_status.ProjectStatusManager( + project=project_name, no_prompt=True, authenticate=False + ) as status_mngr: + status_mngr.token = {} # required, otherwise none + status_mngr.extend_deadline() + + check_output_extend_deadline( + captured_output=capsys.readouterr(), caplog_tuples=caplog.record_tuples + ) + + +def test_extend_deadline_no_available( + capsys: CaptureFixture, monkeypatch, caplog: LogCaptureFixture +): + """If the project is not in status available the operation should fail""" + + confirmed = False + caplog.set_level(logging.INFO) + + # Create mocker + with Mocker() as mock: + # set confirmation object to false + monkeypatch.setattr("rich.prompt.Confirm.ask", lambda question: confirmed) + # Set number of days to extend deadline - Bc of the default value in the function we need to create a mock answer + with unittest.mock.patch("rich.prompt.IntPrompt.ask") as deadline: + deadline.return_value = 2 + + # Create first mocked request - not confirmed + mock.patch( + DDSEndpoint.UPDATE_PROJ_STATUS, + status_code=200, + json=returned_response_extend_deadline_fetch_information_in_progress, + ) + + with pytest.raises(DDSCLIException) as err: + with project_status.ProjectStatusManager( + project=project_name, no_prompt=True, authenticate=False + ) as status_mngr: + status_mngr.token = {} # required, otherwise none + status_mngr.extend_deadline() + + assert ( + "You can only extend the deadline for a project that has the status 'Available'." + in str(err.value) + ) + + +def test_extend_deadline_confirmed_ok( + capsys: CaptureFixture, monkeypatch, caplog: LogCaptureFixture +): + """test that the operation is performed - ok""" + + confirmed = True + caplog.set_level(logging.INFO) + + # Create mocker + with Mocker() as mock: + # set confirmation object to true + monkeypatch.setattr("rich.prompt.Confirm.ask", lambda question: confirmed) + # Set number of days to extend deadline - Bc of the default value in the function we need to create a mock answer + with unittest.mock.patch("rich.prompt.IntPrompt.ask") as deadline: + deadline.return_value = default_unit_days - 1 + + # Mock a dyanic request, the second call should return a different response thatn the first one (operation is confirmed) + mock.patch( + DDSEndpoint.UPDATE_PROJ_STATUS, + [ + { + "status_code": 200, + "json": returned_response_extend_deadline_fetch_information, + }, + {"status_code": 200, "json": returned_response_extend_deadline_ok}, + ], + ) + + with project_status.ProjectStatusManager( + project=project_name, no_prompt=True, authenticate=False + ) as status_mngr: + status_mngr.token = {} # required, otherwise none + status_mngr.extend_deadline() + + captured_output = capsys.readouterr() + assert ( + "dds_cli.project_status", + logging.INFO, + returned_response_extend_deadline_ok["message"], + ) in caplog.record_tuples + check_output_extend_deadline(captured_output=captured_output, caplog_tuples=None) + + +def test_extend_deadline_no_msg_returned_request( + capsys: CaptureFixture, monkeypatch, caplog: LogCaptureFixture +): + """Error - no message returned from request""" + + confirmed = True + caplog.set_level(logging.INFO) + + # Create mocker + with Mocker() as mock: + # set confirmation object to true + monkeypatch.setattr("rich.prompt.Confirm.ask", lambda question: confirmed) + # Set number of days to extend deadline - Bc of the default value in the function we need to create a mock answer + with unittest.mock.patch("rich.prompt.IntPrompt.ask") as deadline: + deadline.return_value = 1 + + # Mock a dyanic request, the second call should return a different response thatn the first one (operation is confirmed) + mock.patch( + DDSEndpoint.UPDATE_PROJ_STATUS, + [ + { + "status_code": 200, + "json": returned_response_extend_deadline_fetch_information, + }, + {"status_code": 200, "json": {}}, # empty response + ], + ) + + with pytest.raises(DDSCLIException) as err: + with project_status.ProjectStatusManager( + project=project_name, no_prompt=True, authenticate=False + ) as status_mngr: + status_mngr.token = {} # required, otherwise none + status_mngr.extend_deadline() + + captured_output = capsys.readouterr() + check_output_extend_deadline(captured_output=captured_output, caplog_tuples=None) + assert "No message returned from API." in str(err.value)