2d0e2c64e6
- tests/: 107 unit tests across config, cache, docker_auth, storage, and FastAPI routes; all passing under pytest-asyncio auto mode - tox.ini: runs pytest via uvx --with tox-uv tox (py311) - .pre-commit-config.yaml: ruff lint + ruff-format at v0.15.12 - pyproject.toml: pytest config (asyncio_mode=auto), ruff config (line-length=140), tox/pre-commit added to dev extras - Makefile: test/tox/pre-commit targets via uvx --python 3.11 - Source files reformatted by ruff-format (no logic changes)
194 lines
8.4 KiB
Python
194 lines
8.4 KiB
Python
"""Tests for RedisCache, focusing on is_index_file with configurable patterns."""
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from artifactapi.cache import RedisCache
|
|
from artifactapi.config import _PACKAGE_INDEX_PATTERNS
|
|
|
|
|
|
@pytest.fixture
|
|
def bare_cache():
|
|
"""RedisCache instance bypassing __init__ (no Redis needed for pure-logic tests)."""
|
|
return RedisCache.__new__(RedisCache)
|
|
|
|
|
|
@pytest.fixture
|
|
def unavailable_cache():
|
|
"""RedisCache where Redis is not reachable."""
|
|
with patch("redis.from_url", side_effect=Exception("connection refused")):
|
|
return RedisCache("redis://localhost:6379/0")
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_redis_client():
|
|
return MagicMock()
|
|
|
|
|
|
@pytest.fixture
|
|
def cache_with_redis(mock_redis_client):
|
|
"""RedisCache backed by a MagicMock Redis client."""
|
|
with patch("redis.from_url", return_value=mock_redis_client):
|
|
c = RedisCache("redis://localhost:6379/0")
|
|
c.client = mock_redis_client
|
|
c.available = True
|
|
return c
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# is_index_file — alpine patterns
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestIsIndexFileAlpine:
|
|
def test_apkindex_tarball_is_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["alpine"]
|
|
assert bare_cache.is_index_file("alpine/v3.18/x86_64/APKINDEX.tar.gz", patterns)
|
|
|
|
def test_nested_apkindex_is_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["alpine"]
|
|
assert bare_cache.is_index_file("mirrors/dl-cdn/alpine/v3.19/community/x86_64/APKINDEX.tar.gz", patterns)
|
|
|
|
def test_apk_package_is_not_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["alpine"]
|
|
assert not bare_cache.is_index_file("alpine/v3.18/x86_64/musl-1.2.4-r2.apk", patterns)
|
|
|
|
def test_random_tarball_is_not_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["alpine"]
|
|
assert not bare_cache.is_index_file("some/path/archive.tar.gz", patterns)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# is_index_file — rpm patterns
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestIsIndexFileRpm:
|
|
def test_repomd_xml_is_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["rpm"]
|
|
assert bare_cache.is_index_file("almalinux/9/x86_64/repomd.xml", patterns)
|
|
|
|
def test_repodata_primary_xml_gz_is_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["rpm"]
|
|
assert bare_cache.is_index_file("repo/repodata/primary.xml.gz", patterns)
|
|
|
|
def test_repodata_sqlite_is_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["rpm"]
|
|
assert bare_cache.is_index_file("repo/repodata/primary.sqlite", patterns)
|
|
|
|
def test_repodata_sqlite_bz2_is_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["rpm"]
|
|
assert bare_cache.is_index_file("repo/repodata/other.sqlite.bz2", patterns)
|
|
|
|
def test_repodata_yaml_xz_is_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["rpm"]
|
|
assert bare_cache.is_index_file("repo/repodata/comps.yaml.xz", patterns)
|
|
|
|
def test_packages_gz_is_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["rpm"]
|
|
assert bare_cache.is_index_file("debian/dists/stable/main/binary-amd64/Packages.gz", patterns)
|
|
|
|
def test_rpm_package_is_not_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["rpm"]
|
|
assert not bare_cache.is_index_file("almalinux/9/x86_64/Packages/bash-5.1.8.x86_64.rpm", patterns)
|
|
|
|
def test_arbitrary_xml_outside_repodata_is_not_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["rpm"]
|
|
assert not bare_cache.is_index_file("some/path/config.xml", patterns)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# is_index_file — docker patterns
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestIsIndexFileDocker:
|
|
def test_tag_manifest_is_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["docker"]
|
|
assert bare_cache.is_index_file("library/nginx/manifests/latest", patterns)
|
|
|
|
def test_version_tag_manifest_is_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["docker"]
|
|
assert bare_cache.is_index_file("library/nginx/manifests/1.25.3", patterns)
|
|
|
|
def test_digest_manifest_is_not_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["docker"]
|
|
digest = "sha256:" + "a" * 64
|
|
assert not bare_cache.is_index_file(f"library/nginx/manifests/{digest}", patterns)
|
|
|
|
def test_tags_list_is_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["docker"]
|
|
assert bare_cache.is_index_file("library/nginx/tags/list", patterns)
|
|
|
|
def test_blob_is_not_index(self, bare_cache):
|
|
patterns = _PACKAGE_INDEX_PATTERNS["docker"]
|
|
assert not bare_cache.is_index_file("library/nginx/blobs/sha256:abc123", patterns)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# is_index_file — edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestIsIndexFileEdgeCases:
|
|
def test_empty_patterns_nothing_is_index(self, bare_cache):
|
|
assert not bare_cache.is_index_file("APKINDEX.tar.gz", [])
|
|
assert not bare_cache.is_index_file("repomd.xml", [])
|
|
assert not bare_cache.is_index_file("library/nginx/manifests/latest", [])
|
|
|
|
def test_none_patterns_nothing_is_index(self, bare_cache):
|
|
assert not bare_cache.is_index_file("APKINDEX.tar.gz", None)
|
|
assert not bare_cache.is_index_file("repomd.xml", None)
|
|
|
|
def test_custom_patterns_match(self, bare_cache):
|
|
patterns = [r"metadata\.json$", r"index\.yaml$"]
|
|
assert bare_cache.is_index_file("repo/metadata.json", patterns)
|
|
assert bare_cache.is_index_file("repo/subdir/index.yaml", patterns)
|
|
assert not bare_cache.is_index_file("repo/data.tar.gz", patterns)
|
|
|
|
def test_custom_pattern_does_not_match_standard_index(self, bare_cache):
|
|
patterns = [r"metadata\.json$"]
|
|
assert not bare_cache.is_index_file("APKINDEX.tar.gz", patterns)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_index_cache_key
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestGetIndexCacheKey:
|
|
def test_same_inputs_produce_same_key(self, bare_cache):
|
|
k1 = bare_cache.get_index_cache_key("alpine-test", "alpine/v3.18/x86_64/APKINDEX.tar.gz")
|
|
k2 = bare_cache.get_index_cache_key("alpine-test", "alpine/v3.18/x86_64/APKINDEX.tar.gz")
|
|
assert k1 == k2
|
|
|
|
def test_different_paths_produce_different_keys(self, bare_cache):
|
|
k1 = bare_cache.get_index_cache_key("alpine-test", "alpine/v3.18/x86_64/APKINDEX.tar.gz")
|
|
k2 = bare_cache.get_index_cache_key("alpine-test", "alpine/v3.19/x86_64/APKINDEX.tar.gz")
|
|
assert k1 != k2
|
|
|
|
def test_different_remotes_produce_different_keys(self, bare_cache):
|
|
k1 = bare_cache.get_index_cache_key("remote-a", "path/to/APKINDEX.tar.gz")
|
|
k2 = bare_cache.get_index_cache_key("remote-b", "path/to/APKINDEX.tar.gz")
|
|
assert k1 != k2
|
|
|
|
def test_key_starts_with_index_prefix_and_remote(self, bare_cache):
|
|
key = bare_cache.get_index_cache_key("myremote", "some/path")
|
|
assert key.startswith("index:myremote:")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# mark_index_cached / is_index_valid
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestIndexValidity:
|
|
def test_marked_then_valid(self, cache_with_redis, mock_redis_client):
|
|
mock_redis_client.exists.return_value = 1
|
|
cache_with_redis.mark_index_cached("remote", "path/APKINDEX.tar.gz", 300)
|
|
assert cache_with_redis.is_index_valid("remote", "path/APKINDEX.tar.gz")
|
|
|
|
def test_missing_key_is_not_valid(self, cache_with_redis, mock_redis_client):
|
|
mock_redis_client.exists.return_value = 0
|
|
assert not cache_with_redis.is_index_valid("remote", "path/APKINDEX.tar.gz")
|
|
|
|
def test_unavailable_redis_is_not_valid(self, unavailable_cache):
|
|
assert not unavailable_cache.is_index_valid("remote", "some/path")
|
|
|
|
def test_mark_cached_no_op_when_unavailable(self, unavailable_cache):
|
|
unavailable_cache.mark_index_cached("remote", "some/path", 300) # must not raise
|