From beb2b5128eda9cfb4539667f3fb18356b1be1a84 Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 7 Jul 2026 21:01:03 +0200 Subject: [PATCH] Reach 100% combined coverage with targeted unit tests. Add fast unit tests for lazy-import helpers and injected-client connect paths that integration tests miss, and raise the coverage floor to 100%. Co-authored-by: Cursor --- pyproject.toml | 2 +- tests/unit/adapters_test.py | 21 +++++++++++++++ tests/unit/minio_adapter_test.py | 40 ++++++++++++++++++++++++++++ tests/unit/redis_adapter_test.py | 45 ++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d9f2ef3..0451106 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ markers = [ source = ["python_repositories"] [tool.coverage.report] -fail_under = 90 +fail_under = 100 show_missing = true precision = 2 diff --git a/tests/unit/adapters_test.py b/tests/unit/adapters_test.py index ed3ac5f..22b14dc 100644 --- a/tests/unit/adapters_test.py +++ b/tests/unit/adapters_test.py @@ -117,3 +117,24 @@ def test_adapters_subpackage_lazy_import_succeeds() -> None: from python_repositories.adapters import RedisAdapter assert RedisAdapter.__name__ == "RedisAdapter" + + +def test_adapters_dir_exposes_lazy_exports() -> None: + """dir(adapters) includes lazy adapter names for tab completion.""" + import python_repositories.adapters as adapters + + assert {"RedisAdapter", "MinioAdapter"}.issubset(set(dir(adapters))) + + +def test_adapters_getattr_raises_for_unknown() -> None: + """Unknown adapter names raise AttributeError.""" + import python_repositories.adapters as adapters + + with pytest.raises(AttributeError, match="has no attribute 'NoSuchAdapter'"): + _ = adapters.NoSuchAdapter + + +def test_top_level_dir_exposes_lazy_exports() -> None: + """dir(python_repositories) includes lazy adapter names for tab completion.""" + assert "RedisAdapter" in dir(python_repositories) + assert "MinioAdapter" in dir(python_repositories) diff --git a/tests/unit/minio_adapter_test.py b/tests/unit/minio_adapter_test.py index 05cdaf2..fdcc73c 100644 --- a/tests/unit/minio_adapter_test.py +++ b/tests/unit/minio_adapter_test.py @@ -53,3 +53,43 @@ def test_raises_when_client_provided_without_config() -> None: mock_client = MagicMock(spec=Minio) with pytest.raises(ValueError, match="config is required"): MinioAdapter(client=mock_client) + + +def test_connect_with_injected_client_succeeds() -> None: + mock_client = MagicMock(spec=Minio) + adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client) + + adapter.connect() + + mock_client.list_buckets.assert_called_once() + + +def test_connect_with_injected_client_raises_on_failure() -> None: + mock_client = MagicMock(spec=Minio) + mock_client.list_buckets.side_effect = Exception("connection lost") + adapter = MinioAdapter(config=TEST_MINIO_CONFIG, client=mock_client) + + with pytest.raises(ConnectionError, match="Could not connect to Minio"): + adapter.connect() + + +def test_connect_disconnects_before_reconnect( + monkeypatch: pytest.MonkeyPatch, +) -> None: + stale_client = MagicMock(spec=Minio) + new_client = MagicMock(spec=Minio) + adapter = MinioAdapter(config=TEST_MINIO_CONFIG) + adapter._client = stale_client + adapter._bucket_name = None + + monkeypatch.setattr( + "python_repositories.adapters.minio_adapter.minio.Minio", + lambda *args, **kwargs: new_client, + ) + + adapter.connect() + + assert adapter._client is new_client + assert adapter._bucket_name == TEST_MINIO_CONFIG.bucket + new_client.list_buckets.assert_called_once() + new_client.bucket_exists.assert_called_once_with(TEST_MINIO_CONFIG.bucket) diff --git a/tests/unit/redis_adapter_test.py b/tests/unit/redis_adapter_test.py index 9e036be..140d45e 100644 --- a/tests/unit/redis_adapter_test.py +++ b/tests/unit/redis_adapter_test.py @@ -69,3 +69,48 @@ def test_subclass_custom_env_var_name(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("CUSTOM_REDIS_URI", "redis://custom:6379") adapter = CustomEnvRedisAdapter() assert adapter._config.uri == "redis://custom:6379" + + +def test_connect_with_injected_client_succeeds_when_ping_ok() -> None: + mock_client = MagicMock(spec=redis.Redis) + mock_client.ping.return_value = True + adapter = RedisAdapter(config=TEST_REDIS_CONFIG, client=mock_client) + + adapter.connect() + + mock_client.ping.assert_called_once() + + +def test_connect_with_injected_client_raises_when_ping_false() -> None: + mock_client = MagicMock(spec=redis.Redis) + mock_client.ping.return_value = False + adapter = RedisAdapter(config=TEST_REDIS_CONFIG, client=mock_client) + + with pytest.raises(ConnectionError, match="Could not connect to Redis"): + adapter.connect() + + +def test_connect_with_injected_client_raises_on_redis_error() -> None: + mock_client = MagicMock(spec=redis.Redis) + mock_client.ping.side_effect = redis.ConnectionError("connection lost") + adapter = RedisAdapter(config=TEST_REDIS_CONFIG, client=mock_client) + + with pytest.raises(ConnectionError, match="Could not connect to Redis"): + adapter.connect() + + +def test_connect_closes_existing_non_injected_client( + monkeypatch: pytest.MonkeyPatch, +) -> None: + stale_client = MagicMock(spec=redis.Redis) + new_client = MagicMock(spec=redis.Redis) + new_client.ping.return_value = True + adapter = RedisAdapter(config=TEST_REDIS_CONFIG) + adapter._client = stale_client + + monkeypatch.setattr("redis.Redis.from_url", lambda *args, **kwargs: new_client) + + adapter.connect() + + stale_client.close.assert_called_once() + assert adapter._client is new_client