6974b3620f
ci / lint (push) Successful in 24s
ci / types (push) Successful in 35s
ci / unit (push) Successful in 26s
ci / security (push) Successful in 41s
ci / dockerfile (push) Successful in 8s
ci / chart (push) Successful in 9s
ci / integration (push) Successful in 46s
ci / image (api) (push) Successful in 2m32s
ci / image (reconciler) (push) Successful in 2m50s
ci / image (worker) (push) Successful in 2m43s
ci / bump (push) Successful in 21s
The three existing entries are sized like real products: `elasticsearch` small asks for 1Gi and medium for 4Gi across three replicas. On this cluster that is a request that never schedules, so provisioning them proves something about the node and nothing about svcforge. podinfo 16Mi/10m — one small Go binary, no dependencies, no PVC nginx 32Mi/10m — recognisable, still small Both are addressed as `oci://`, which is load-bearing rather than cosmetic. An OCI chart is pulled by reference with no `helm repo add` first. The three existing entries name `bitnamilegacy/<chart>`, a classic repo alias that nothing in the worker image configures — so as written they cannot resolve at provision time. OCI is the form that works from a bare container, and it is why the e2e test already provisions podinfo. Chart versions were resolved against the real registries before committing (podinfo 6.7.1, nginx 25.0.14 / app 1.31.3) rather than guessed, since a wrong version fails only at provision time. Also updates the CLI ServiceType enum, the catalog test's expected set, and the service list in the OpenAPI description.
156 lines
4.6 KiB
Python
156 lines
4.6 KiB
Python
"""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"}
|