"""Unit tests for catalog loading and validation.""" import textwrap from pathlib import Path import pytest from svcforge_core.domain.catalog import CatalogError, load_catalog from svcforge_core.domain.models import CatalogEntry VALID_YAML = textwrap.dedent(""" services: redis: chart: bitnamilegacy/redis chart_version: 20.6.2 sizes: small: replicas: 1 resources: requests: {cpu: 100m, memory: 256Mi} medium: replicas: 3 resources: requests: {cpu: 500m, memory: 1Gi} postgres: chart: bitnamilegacy/postgresql chart_version: 16.4.5 sizes: small: replicas: 1 resources: requests: {cpu: 250m, memory: 512Mi} """) MISSING_CHART_VERSION_YAML = textwrap.dedent(""" services: redis: chart: bitnamilegacy/redis sizes: small: replicas: 1 resources: {} """) ZERO_REPLICAS_YAML = textwrap.dedent(""" services: redis: chart: bitnamilegacy/redis chart_version: 20.6.2 sizes: small: replicas: 0 resources: {} """) def _write(tmp_path: Path, body: str) -> Path: path = tmp_path / "catalog.yaml" path.write_text(body) return path def test_valid_yaml_loads_to_catalog_entries(tmp_path: Path) -> None: catalog = load_catalog(_write(tmp_path, VALID_YAML)) assert set(catalog) == {"redis", "postgres"} assert all(isinstance(entry, CatalogEntry) for entry in catalog.values()) redis = catalog["redis"] assert redis.service_type == "redis" assert redis.chart_version == "20.6.2" assert set(redis.sizes) == {"small", "medium"} assert redis.sizes["medium"].replicas == 3 def test_missing_chart_version_raises_catalog_error(tmp_path: Path) -> None: with pytest.raises(CatalogError) as excinfo: load_catalog(_write(tmp_path, MISSING_CHART_VERSION_YAML)) assert excinfo.value.key == "redis" assert "redis" in str(excinfo.value) def test_zero_replicas_raises_catalog_error(tmp_path: Path) -> None: with pytest.raises(CatalogError) as excinfo: load_catalog(_write(tmp_path, ZERO_REPLICAS_YAML)) assert excinfo.value.key == "redis" def test_missing_file_raises_catalog_error(tmp_path: Path) -> None: with pytest.raises(CatalogError, match="cannot read catalog"): load_catalog(tmp_path / "nope.yaml") def test_unparseable_yaml_raises_catalog_error(tmp_path: Path) -> None: with pytest.raises(CatalogError, match="not valid YAML"): load_catalog(_write(tmp_path, "services: [unclosed\n")) def test_scalar_root_raises_catalog_error(tmp_path: Path) -> None: with pytest.raises(CatalogError, match="must be a mapping"): load_catalog(_write(tmp_path, "just-a-string\n")) def test_non_mapping_services_raises_catalog_error(tmp_path: Path) -> None: with pytest.raises(CatalogError, match="'services' must be a mapping"): load_catalog(_write(tmp_path, "services:\n - redis\n")) def test_non_mapping_entry_raises_catalog_error_naming_the_key(tmp_path: Path) -> None: with pytest.raises(CatalogError) as excinfo: load_catalog(_write(tmp_path, "services:\n redis: just-a-string\n")) assert excinfo.value.key == "redis" assert "must be a mapping" in str(excinfo.value) def test_non_string_field_key_raises_catalog_error_naming_the_key(tmp_path: Path) -> None: """A non-string YAML key inside an entry breaks `**body`; it must surface as CatalogError.""" body = textwrap.dedent(""" services: redis: 1: oops chart: bitnamilegacy/redis chart_version: 20.6.2 sizes: {} """) with pytest.raises(CatalogError) as excinfo: load_catalog(_write(tmp_path, body)) assert excinfo.value.key == "redis" def test_bare_mapping_without_services_key_is_accepted(tmp_path: Path) -> None: """The top-level `services:` wrapper is optional; a bare service_type mapping also loads.""" body = textwrap.dedent(""" redis: chart: bitnamilegacy/redis chart_version: 20.6.2 sizes: small: replicas: 1 resources: {} """) catalog = load_catalog(_write(tmp_path, body)) assert set(catalog) == {"redis"} def test_repo_catalog_yaml_is_valid() -> None: catalog = load_catalog(Path(__file__).parents[2] / "catalog.yaml") assert set(catalog) == {"elasticsearch", "redis", "postgres", "podinfo", "nginx"} for entry in catalog.values(): assert set(entry.sizes) == {"small", "medium"} def test_entry_values_default_to_empty(tmp_path: Path) -> None: """`values:` is optional — an entry that needs no chart knobs says nothing.""" body = textwrap.dedent(""" services: podinfo: chart: oci://ghcr.io/stefanprodan/charts/podinfo chart_version: "6.14.0" sizes: small: {replicas: 1, resources: {}} """) assert load_catalog(_write(tmp_path, body))["podinfo"].values == {} def test_entry_values_are_parsed(tmp_path: Path) -> None: """Nested values survive the load, which is what `global.imageRegistry` needs.""" body = textwrap.dedent(""" services: redis: chart: oci://mirror.gcr.io/bitnamicharts/redis chart_version: "27.0.15" values: global: imageRegistry: mirror.gcr.io sizes: small: {replicas: 1, resources: {}} """) assert load_catalog(_write(tmp_path, body))["redis"].values == { "global": {"imageRegistry": "mirror.gcr.io"} } def test_no_catalog_entry_pulls_from_docker_hub() -> None: """Every chart, and every image registry an entry pins, avoids Docker Hub. Docker Hub rate-limits anonymous pulls per source IP and the whole cluster shares one NAT address, so a Docker Hub reference here is a provision that fails under load for a reason no log in this repo will explain. """ catalog = load_catalog(Path(__file__).parents[2] / "catalog.yaml") banned = ("docker.io", "registry-1.docker.io", "index.docker.io") for name, entry in catalog.items(): assert not entry.chart.startswith(banned), f"{name}: chart on Docker Hub" assert "docker.io" not in entry.chart, f"{name}: chart on Docker Hub" registry = entry.values.get("global", {}).get("imageRegistry") # A bitnami chart defaults its images to Docker Hub, so any entry pointing at one # has to redirect them. podinfo's chart already names ghcr.io and needs nothing. if "bitnamicharts" in entry.chart: assert registry == "mirror.gcr.io", f"{name}: bitnami chart without a registry override" if registry is not None: assert "docker.io" not in registry, f"{name}: imageRegistry on Docker Hub"