Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/lint-pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
# If new Python versions are added, update pylint accordingly
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -23,7 +24,11 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pylint==2.17.3
pip install setuptools wheel
pip install pylint==3.3.0
- name: Analysing the code with pylint
# Disable rule that limits to 5 the number of positional arguments
run: |
pylint --rcfile=.pylintrc $(git ls-files '*.py')
pylint --rcfile=.pylintrc \
--disable=too-many-positional-arguments \
-- $(git ls-files '*.py')
1 change: 1 addition & 0 deletions SPRINTLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,3 +472,4 @@ _Empty sprint_
## 2025-09-15 - 2025-09-26

- Bump cryptography library from 42.0.4 to 44.0.1 to solve vulnerabities ([#845](https://github.com/ScilifelabDataCentre/dds_cli/pull/845))
- Fix style according to guidelines and drop support for python 3.8 and 3.9 due to CVE, added new tests ([#843](https://github.com/ScilifelabDataCentre/dds_cli/pull/843))
4 changes: 2 additions & 2 deletions WINDOWS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# How to install the DDS CLI on Windows

## 1. Download and install Python > 3.8
## 1. Download and install Python > 3.10

### 1.1. Go to https://www.python.org/downloads/windows/

Expand Down Expand Up @@ -42,7 +42,7 @@

![Powershell 1](docs/_static/windows/powershell_top-1.png)

> **Note:** Upon entering `python --version` the version of the previously installed Python distribution should be shown. If a version number < 3.8 is shown or Python is not found, please consult the :ref:`Troubleshooting<troubleshooting>` section below.
> **Note:** Upon entering `python --version` the version of the previously installed Python distribution should be shown. If a version number < 3.10 is shown or Python is not found, please consult the :ref:`Troubleshooting<troubleshooting>` section below.

### 2.3. Python ships with a helper program called **pip** to install additional packages. In the next step, this software should be upgraded to its current version.

Expand Down
9 changes: 7 additions & 2 deletions dds_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,13 +544,14 @@ def configure():
"Which method would you like to use?", choices=["Email", "Authenticator App", "Cancel"]
).ask()

auth_method: str | None = None # type hint, initialized
if auth_method_choice == "Cancel":
LOG.info("Two-factor authentication method not configured.")
sys.exit(0)
elif auth_method_choice == "Authenticator App":
auth_method: str = "totp"
auth_method = "totp"
elif auth_method_choice == "Email":
auth_method: str = "hotp"
auth_method = "hotp"

with dds_cli.auth.Auth(authenticate=True, force_renew_token=False) as authenticator:
authenticator.twofactor(auth_method=auth_method)
Expand Down Expand Up @@ -885,6 +886,8 @@ def activate_user(click_ctx, email):
Super Admins: All users
Unit Admins: Unit Admins / Personnel
"""
proceed_activation = False # default assignment

if click_ctx.get("NO_PROMPT", False):
pass
else:
Expand Down Expand Up @@ -925,6 +928,8 @@ def deactivate_user(click_ctx, email):
Super Admins: All users
Unit Admins: Unit Admins / Personnel
"""
proceed_deactivation = False # default assignment

if click_ctx.get("NO_PROMPT", False):
pass
else:
Expand Down
2 changes: 2 additions & 0 deletions dds_cli/custom_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ def create_and_remove_task(self, *args, **kwargs):
SpinnerColumn(spinner_name="dots12", style="white"),
console=dds_cli.utils.stderr_console,
) as progress:

description: str | None = None # type hint, initialized
# Determine spinner text
if func.__name__ == "remove_all":
description = f"Removing all files in project {self.project}"
Expand Down
3 changes: 1 addition & 2 deletions dds_cli/file_compressor.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,7 @@ def compress_file(
# if not chunk:
# break
# yield
for chunk in iter(lambda: compressor.read(chunk_size), b""):
yield chunk
yield from iter(lambda: compressor.read(chunk_size), b"")
except Exception as err: # pylint: disable=broad-exception-caught
LOG.warning(str(err))
else:
Expand Down
12 changes: 5 additions & 7 deletions dds_cli/file_handler_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,7 @@ def read_file(file, chunk_size: int = FileSegment.SEGMENT_SIZE_RAW):

try:
with file.open(mode="rb") as infile:
for chunk in iter(lambda: infile.read(chunk_size), b""):
yield chunk
yield from iter(lambda: infile.read(chunk_size), b"")
except OSError as err:
LOG.warning(str(err))

Expand Down Expand Up @@ -278,11 +277,10 @@ def stream_from_file(self, file):
# LOG.debug(
# "Test: %s",
# )
for chunk in fc.Compressor.compress_file(file=file_info["path_raw"]):
# LOG.debug("Chunk type: %s", type(chunk))
# checksum.update(chunk)
# break
yield chunk
yield from fc.Compressor.compress_file(file=file_info["path_raw"])
# LOG.debug("Chunk type: %s", type(chunk))
# checksum.update(chunk)
# break

# LOG.debug("Streaming file finished.")
# Add checksum to file info
Expand Down
4 changes: 2 additions & 2 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ As mentioned above, you can install ``dds-cli`` from PyPI or via an executable.
Install from **PyPI**
-----------------------

1. To perform these steps you need to have Python version 3.8 or higher installed.
1. To perform these steps you need to have Python version 3.10 or higher installed.

* First check which Python version you have

Expand All @@ -39,7 +39,7 @@ Install from **PyPI**

.. image:: ../img/python3-version.svg

If this does not return ``Python 3.8.x`` or higher, you will need to `install Python <https://www.python.org/downloads/>`_.
If this does not return ``Python 3.10.x`` or higher, you will need to `install Python <https://www.python.org/downloads/>`_.

.. warning::

Expand Down
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@
classifiers=[
"Development Status :: 4 - Beta",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
],
url="https://github.com/ScilifelabDataCentre/dds_cli",
author="SciLifeLab Data Centre",
Expand Down
184 changes: 184 additions & 0 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"""Tests for CLI commands in dds_cli.__main__"""

from click.testing import CliRunner
from unittest.mock import MagicMock, patch
import pytest


import dds_cli.exceptions
from dds_cli.__main__ import dds_main


#### AUTH COMMANDS #####

## TWOFACTOR subcommands ##


# parametrized options to pass to the test
@pytest.mark.parametrize(
"user_choice, expected_auth_method, expected_exit_code",
[
("Email", "hotp", 0),
("Authenticator App", "totp", 0),
("Cancel", None, 0), # Cancel exits with 0
],
)
def test_auth_configure_ok(user_choice, expected_auth_method, expected_exit_code):
"""Test of configure two factor method - ok"""

runner = CliRunner()
with patch("dds_cli.__main__.questionary.select") as mock_select, patch(
"dds_cli.auth.Auth"
) as mock_auth:

# Mock the user selecting option
mock_select.return_value.ask.return_value = user_choice

# Mock the Auth context manager
mock_auth_instance = MagicMock()
mock_auth.return_value.__enter__.return_value = mock_auth_instance

result = runner.invoke(dds_main, ["auth", "twofactor", "configure"])

assert result.exit_code == expected_exit_code

if user_choice == "Cancel":
# No call to twofactor
mock_auth_instance.twofactor.assert_not_called()
else:
mock_auth_instance.twofactor.assert_called_once_with(auth_method=expected_auth_method)


# parametrized options to pass to the test
@pytest.mark.parametrize("user_choice", ["Email", "Authenticator App"])
@pytest.mark.parametrize(
"exc_type", [dds_cli.exceptions.DDSCLIException, dds_cli.exceptions.ApiResponseError]
)
def test_auth_configure_exceptions(exc_type, user_choice):
"""Test of configure two factor - fails with exceptions"""

runner = CliRunner()
with patch("dds_cli.__main__.questionary.select") as mock_select, patch(
"dds_cli.auth.Auth"
) as mock_auth:

# Mock the user selecting option
mock_select.return_value.ask.return_value = user_choice

# Mock Auth context manager to raise exception
mock_auth_instance = MagicMock()
mock_auth.return_value.__enter__.return_value = mock_auth_instance
mock_auth_instance.twofactor.side_effect = exc_type("message")

result = runner.invoke(dds_main, ["auth", "twofactor", "configure"])

assert result.exit_code == 1


#### USER COMMANDS #####

## Activate and deativate users ##


@pytest.mark.parametrize("confirm", [True, False])
def test_user_activate_confirm_ok(confirm):
"""Test user activation when confirmation is yes/no. - no errors"""

runner = CliRunner()
with patch(
"dds_cli.__main__.rich.prompt.Confirm.ask", return_value=confirm
) as mock_confirm, patch("dds_cli.account_manager.AccountManager") as mock_manager:

mock_manager_instance = MagicMock()
mock_manager.return_value.__enter__.return_value = mock_manager_instance

result = runner.invoke(dds_main, ["user", "activate", "[email protected]"])

assert result.exit_code == 0
mock_confirm.assert_called_once()

if confirm:
mock_manager_instance.user_activation.assert_called_once_with(
email="[email protected]", action="reactivate"
)
else:
mock_manager_instance.user_activation.assert_not_called()


@pytest.mark.parametrize("confirm", [True, False])
def test_user_deactivate_confirm_ok(confirm):
"""Test user de-activation when confirmation is yes/no. - no errors"""

runner = CliRunner()
with patch(
"dds_cli.__main__.rich.prompt.Confirm.ask", return_value=confirm
) as mock_confirm, patch("dds_cli.account_manager.AccountManager") as mock_manager:

mock_manager_instance = MagicMock()
mock_manager.return_value.__enter__.return_value = mock_manager_instance

result = runner.invoke(dds_main, ["user", "deactivate", "[email protected]"])

assert result.exit_code == 0
mock_confirm.assert_called_once()

if confirm:
mock_manager_instance.user_activation.assert_called_once_with(
email="[email protected]", action="deactivate"
)
else:
mock_manager_instance.user_activation.assert_not_called()


@pytest.mark.parametrize(
"exc_type",
[
dds_cli.exceptions.AuthenticationError,
dds_cli.exceptions.ApiResponseError,
dds_cli.exceptions.ApiRequestError,
dds_cli.exceptions.DDSCLIException,
],
)
def test_user_activate_exceptions(exc_type):
"""Test user activation when AccountManager raises errors."""

runner = CliRunner()
with patch("dds_cli.__main__.rich.prompt.Confirm.ask", return_value=True), patch(
"dds_cli.account_manager.AccountManager"
) as mock_manager:

mock_manager_instance = MagicMock()
mock_manager_instance.user_activation.side_effect = exc_type("error")
mock_manager.return_value.__enter__.return_value = mock_manager_instance

result = runner.invoke(dds_main, ["user", "activate", "[email protected]"])

assert result.exit_code == 1
assert "error" in result.output


@pytest.mark.parametrize(
"exc_type",
[
dds_cli.exceptions.AuthenticationError,
dds_cli.exceptions.ApiResponseError,
dds_cli.exceptions.ApiRequestError,
dds_cli.exceptions.DDSCLIException,
],
)
def test_user_deactivate_exceptions(exc_type):
"""Test user de-activation when AccountManager raises errors."""

runner = CliRunner()
with patch("dds_cli.__main__.rich.prompt.Confirm.ask", return_value=True), patch(
"dds_cli.account_manager.AccountManager"
) as mock_manager:

mock_manager_instance = MagicMock()
mock_manager_instance.user_activation.side_effect = exc_type("error")
mock_manager.return_value.__enter__.return_value = mock_manager_instance

result = runner.invoke(dds_main, ["user", "deactivate", "[email protected]"])

assert result.exit_code == 1
assert "error" in result.output
Loading
Loading