From 14b4f6b5855bafa987ef3bcae010d4847ceea4b3 Mon Sep 17 00:00:00 2001 From: Owen Lamont Date: Mon, 6 Jan 2025 22:17:23 +1030 Subject: [PATCH] Fixed bug with relative paths and improved test coverage --- .pre-commit-config.yaml | 2 +- src/uv_secure/__version__.py | 2 +- .../directory_scanner/directory_scanner.py | 21 +++--- tests/uv_secure/test_run.py | 75 +++++++++++++++++++ uv.lock | 14 ++-- 5 files changed, 96 insertions(+), 18 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4479c3d..1cca976 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: additional_dependencies: - pydantic - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.8.5 + rev: v0.8.6 hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] diff --git a/src/uv_secure/__version__.py b/src/uv_secure/__version__.py index 493f741..260c070 100644 --- a/src/uv_secure/__version__.py +++ b/src/uv_secure/__version__.py @@ -1 +1 @@ -__version__ = "0.3.0" +__version__ = "0.3.1" diff --git a/src/uv_secure/directory_scanner/directory_scanner.py b/src/uv_secure/directory_scanner/directory_scanner.py index a83e520..79d3573 100644 --- a/src/uv_secure/directory_scanner/directory_scanner.py +++ b/src/uv_secure/directory_scanner/directory_scanner.py @@ -1,4 +1,4 @@ -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from typing import Union from anyio import Path @@ -23,15 +23,17 @@ async def _find_files( return {filename: task.value for filename, task in tasks.items()} -async def _get_root_dir(file_paths: Iterable[Path]) -> Path: +async def _resolve_paths(file_paths: Sequence[Path]) -> list[Path]: async with create_task_group() as tg: tasks = [tg.soonify(path.resolve)() for path in file_paths] + return [task.value for task in tasks] - resolved_paths = [task.value for task in tasks] - if len(resolved_paths) == 1: - return resolved_paths[0].parent - split_paths = [list(rp.parts) for rp in resolved_paths] +def _get_root_dir(file_paths: Sequence[Path]) -> Path: + if len(file_paths) == 1: + return file_paths[0].parent + + split_paths = [list(rp.parts) for rp in file_paths] min_length = min(len(parts) for parts in split_paths) common_prefix_len = 0 @@ -61,16 +63,17 @@ async def get_lock_to_config_map( A dictionary mapping uv.lock files to their nearest Configuration """ if type(file_paths) is Path: - root_dir = file_paths + root_dir = await file_paths.resolve() config_and_lock_files = await _find_files( root_dir, ["pyproject.toml", "uv-secure.toml", ".uv-secure.toml", "uv.lock"] ) else: - root_dir = await _get_root_dir(file_paths) + resolved_paths = await _resolve_paths(file_paths) + root_dir = _get_root_dir(resolved_paths) config_and_lock_files = await _find_files( root_dir, ["pyproject.toml", "uv-secure.toml", ".uv-secure.toml"] ) - config_and_lock_files["uv.lock"] = file_paths + config_and_lock_files["uv.lock"] = resolved_paths config_file_paths = ( config_and_lock_files["pyproject.toml"] diff --git a/tests/uv_secure/test_run.py b/tests/uv_secure/test_run.py index 35d4fbf..c3e0087 100644 --- a/tests/uv_secure/test_run.py +++ b/tests/uv_secure/test_run.py @@ -1,5 +1,7 @@ +import os from pathlib import Path +from httpx import Request, RequestError import pytest from pytest_httpx import HTTPXMock from typer.testing import CliRunner @@ -145,6 +147,25 @@ def one_vulnerability_response_v2(httpx_mock: HTTPXMock) -> HTTPXMock: return httpx_mock +@pytest.fixture +def package_version_not_found_response(httpx_mock: HTTPXMock) -> HTTPXMock: + httpx_mock.add_response( + url="https://pypi.org/pypi/example-package/1.0.0/json", status_code=404 + ) + return httpx_mock + + +@pytest.fixture +def missing_vulnerability_response(httpx_mock: HTTPXMock) -> HTTPXMock: + httpx_mock.add_exception( + RequestError( + "Request failed", + request=Request("GET", "https://pypi.org/pypi/example-package/1.0.0/json"), + ) + ) + return httpx_mock + + def test_app_version() -> None: result = runner.invoke(app, "--version") assert result.exit_code == 0 @@ -175,6 +196,60 @@ def test_app_no_vulnerabilities( assert "All dependencies appear safe!" in result.output +def test_app_no_vulnerabilities_relative_lock_file_path( + tmp_path: Path, temp_uv_lock_file: Path, no_vulnerabilities_response: HTTPXMock +) -> None: + """Test check_dependencies with a single dependency and no vulnerabilities.""" + + os.chdir(tmp_path) + result = runner.invoke(app, ["uv.lock"]) + + assert result.exit_code == 0 + assert "No vulnerabilities detected!" in result.output + assert "Checked: 1 dependency" in result.output + assert "All dependencies appear safe!" in result.output + + +def test_app_no_vulnerabilities_relative_no_specified_path( + tmp_path: Path, temp_uv_lock_file: Path, no_vulnerabilities_response: HTTPXMock +) -> None: + """Test check_dependencies with a single dependency and no vulnerabilities.""" + + os.chdir(tmp_path) + result = runner.invoke(app) + + assert result.exit_code == 0 + assert "No vulnerabilities detected!" in result.output + assert "Checked: 1 dependency" in result.output + assert "All dependencies appear safe!" in result.output + + +def test_app_failed_vulnerability_request( + temp_uv_lock_file: Path, missing_vulnerability_response: HTTPXMock +) -> None: + """Test check_dependencies with a single dependency and no vulnerabilities.""" + result = runner.invoke(app, [str(temp_uv_lock_file)]) + + assert result.exit_code == 0 + assert "Error fetching example-package==1.0.0: Request failed" in result.output + assert "No vulnerabilities detected!" in result.output + assert "Checked: 1 dependency" in result.output + assert "All dependencies appear safe!" in result.output + + +def test_app_package_not_found( + temp_uv_lock_file: Path, package_version_not_found_response: HTTPXMock +) -> None: + """Test check_dependencies with a single dependency and no vulnerabilities.""" + result = runner.invoke(app, [str(temp_uv_lock_file)]) + + assert result.exit_code == 0 + assert "Warning: Could not fetch data for example-package==1.0.0" in result.output + assert "No vulnerabilities detected!" in result.output + assert "Checked: 1 dependency" in result.output + assert "All dependencies appear safe!" in result.output + + def test_check_dependencies_with_vulnerability( temp_uv_lock_file: Path, one_vulnerability_response: HTTPXMock ) -> None: diff --git a/uv.lock b/uv.lock index 36fadc9..8a01dd0 100644 --- a/uv.lock +++ b/uv.lock @@ -12,7 +12,7 @@ wheels = [ [[package]] name = "anyio" -version = "4.7.0" +version = "4.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, @@ -20,9 +20,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/40/318e58f669b1a9e00f5c4453910682e2d9dd594334539c7b7817dabb765f/anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48", size = 177076 } +sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/7a/4daaf3b6c08ad7ceffea4634ec206faeff697526421c20f07628c7372156/anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352", size = 93052 }, + { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, ] [[package]] @@ -392,11 +392,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/c0/9c9832e5be227c40e1ce774d493065f83a91d6430baa7e372094e9683a45/pygments-2.19.0.tar.gz", hash = "sha256:afc4146269910d4bdfabcd27c24923137a74d562a23a320a41a55ad303e19783", size = 4967733 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, + { url = "https://files.pythonhosted.org/packages/20/dc/fde3e7ac4d279a331676829af4afafd113b34272393d73f610e8f0329221/pygments-2.19.0-py3-none-any.whl", hash = "sha256:4755e6e64d22161d5b61432c0600c923c5927214e7c956e31c23923c89251a9b", size = 1225305 }, ] [[package]] @@ -564,7 +564,7 @@ wheels = [ [[package]] name = "uv-secure" -version = "0.3.0" +version = "0.3.1" source = { editable = "." } dependencies = [ { name = "anyio" },