Files
ss-tools/backend/tests/scripts/test_publish_catalog.py

188 lines
7.5 KiB
Python

"""Offline tests for the D7-prep Gitea catalog publisher (no network)."""
# #region Test.Tooling.PublishCatalog [C:3] [TYPE Module] [SEMANTICS test,cli,gitea,catalog,publish]
# @BRIEF Offline fixtures for validate/path/fetch/put and the typed failure matrix.
# @RELATION VERIFIES -> [Tooling.PublishCatalog]
# @TEST_CONTRACT: validate_catalog -> parsed dict or PUBLISH_CATALOG_INVALID
# @TEST_EDGE: missing-env config -> exit 2 without network
# @TEST_INVARIANT Tooling.PublishCatalog: an invalid catalog never reaches the network; create and
# update PUTs carry the right sha semantics; auth/conflict failures are typed.
# -> VERIFIED_BY: publish flow tests below
import base64
import json
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src"))
from src.scripts.publish_catalog import ( # noqa: E402
GiteaTarget,
PublishError,
catalog_path,
fetch_existing_sha,
main,
publish_catalog_bytes,
validate_catalog,
)
_CATALOG = {
"catalog_revision": "rev-001",
"baseline_pin": {
"baseline_set_id": "ss-prod-visual", "baseline_set_version": "1",
"release_id": "ss-prod-initial", "baseline_family": "visual",
"catalog_digest": "a" * 64,
},
}
_RAW = json.dumps(_CATALOG).encode()
_TARGET = GiteaTarget(base_url="http://gitea.test", repo="o/r", ref="main", token="t", path="catalogs/ss-prod-visual/v1.json")
class _FakeResponse:
def __init__(self, status_code: int, payload=None, text: str = "") -> None:
self.status_code = status_code
self._payload = payload if payload is not None else {}
self.text = text
def json(self):
return self._payload
class _FakeClient:
def __init__(self, get_response, put_response) -> None:
self.get_response = get_response
self.put_response = put_response
self.gets: list[tuple[str, dict]] = []
self.puts: list[tuple[str, dict]] = []
def get(self, url, params=None):
self.gets.append((url, params or {}))
return self.get_response
def put(self, url, json=None):
self.puts.append((url, json or {}))
return self.put_response
def __enter__(self):
return self
def __exit__(self, *args):
return False
# #endregion Test.Tooling.PublishCatalog.Fakes
# #region Test.Tooling.PublishCatalog.Validate [C:2] [TYPE Function]
def test_validate_catalog_accepts_wellformed_and_rejects_malformed():
assert validate_catalog(_RAW)["baseline_pin"]["catalog_digest"] == "a" * 64
for bad in (b"not-json", b"[]", json.dumps({"baseline_pin": {}}).encode(), json.dumps({"baseline_pin": {"baseline_set_id": "x"}}).encode()):
try:
validate_catalog(bad)
except PublishError as exc:
assert exc.code == "PUBLISH_CATALOG_INVALID"
else:
raise AssertionError(f"catalog {bad[:30]!r} must be rejected")
def test_catalog_path_renders_template_and_default():
assert catalog_path("ss-prod-visual", "1", "catalogs/{baseline_set_id}/v{version}.json") == "catalogs/ss-prod-visual/v1.json"
assert catalog_path("ss-prod-visual", "1", None) == "catalogs/ss-prod-visual/v1.json"
# #endregion Test.Tooling.PublishCatalog.Validate
# #region Test.Tooling.PublishCatalog.Publish [C:2] [TYPE Function]
def test_publish_creates_when_absent_and_updates_with_sha():
create_client = _FakeClient(_FakeResponse(404), _FakeResponse(201, {"content": {"path": "x"}}))
assert fetch_existing_sha(create_client, _TARGET) is None
publish_catalog_bytes(create_client, _TARGET, _RAW, "create")
assert "sha" not in create_client.puts[0][1]
assert base64.b64decode(create_client.puts[0][1]["content"]) == _RAW
update_client = _FakeClient(_FakeResponse(200, {"sha": "existing-sha"}), _FakeResponse(200, {}))
assert fetch_existing_sha(update_client, _TARGET) == "existing-sha"
publish_catalog_bytes(update_client, _TARGET, _RAW, "update")
assert update_client.puts[0][1]["sha"] == "existing-sha"
def test_publish_failures_are_typed():
auth_client = _FakeClient(_FakeResponse(401), _FakeResponse(200, {}))
try:
fetch_existing_sha(auth_client, _TARGET)
except PublishError as exc:
assert exc.code == "PUBLISH_AUTH_FAILED"
else:
raise AssertionError("401 must be PUBLISH_AUTH_FAILED")
conflict_client = _FakeClient(_FakeResponse(200, {"sha": "s"}), _FakeResponse(409))
try:
publish_catalog_bytes(conflict_client, _TARGET, _RAW, "msg")
except PublishError as exc:
assert exc.code == "PUBLISH_CONFLICT"
else:
raise AssertionError("409 must be PUBLISH_CONFLICT")
broken_client = _FakeClient(_FakeResponse(500, text="boom"), _FakeResponse(200, {}))
try:
fetch_existing_sha(broken_client, _TARGET)
except PublishError as exc:
assert exc.code == "PUBLISH_UNAVAILABLE"
else:
raise AssertionError("500 must be PUBLISH_UNAVAILABLE")
# #endregion Test.Tooling.PublishCatalog.Publish
# #region Test.Tooling.PublishCatalog.Main [C:2] [TYPE Function]
def test_main_missing_config_exits_2_without_network(monkeypatch, tmp_path):
catalog_file = tmp_path / "catalog.json"
catalog_file.write_bytes(_RAW)
monkeypatch.delenv("PUBLISHED_CATALOG_GITEA_URL", raising=False)
monkeypatch.delenv("PUBLISHED_CATALOG_GITEA_TOKEN", raising=False)
monkeypatch.delenv("PUBLISHED_CATALOG_GITEA_REPO", raising=False)
assert main(["--catalog", str(catalog_file), "--baseline-set", "ss-prod-visual", "--version", "1"]) == 2
def test_main_publishes_end_to_end_with_fake_client(monkeypatch, tmp_path):
import src.scripts.publish_catalog as module
catalog_file = tmp_path / "catalog.json"
catalog_file.write_bytes(_RAW)
monkeypatch.setenv("PUBLISHED_CATALOG_GITEA_URL", "http://gitea.test")
monkeypatch.setenv("PUBLISHED_CATALOG_GITEA_TOKEN", "token-value")
monkeypatch.setenv("PUBLISHED_CATALOG_GITEA_REPO", "o/r")
seen: dict = {}
class _InstantClient(_FakeClient):
def __init__(self, **kwargs) -> None:
super().__init__(_FakeResponse(200, {"sha": "s"}), _FakeResponse(201, {"commit": {"id": "c1"}}))
seen["kwargs"] = kwargs
monkeypatch.setattr(module.httpx, "Client", lambda **kwargs: _InstantClient(**kwargs))
code = main(["--catalog", str(catalog_file), "--baseline-set", "ss-prod-visual", "--version", "1",
"--message", "publish test"])
assert code == 0
assert seen["kwargs"]["base_url"] == "http://gitea.test"
assert seen["kwargs"]["headers"] == {"Authorization": "token token-value"}
def test_main_types_transport_errors(monkeypatch, tmp_path, capsys):
import httpx
import src.scripts.publish_catalog as module
catalog_file = tmp_path / "catalog.json"
catalog_file.write_bytes(_RAW)
monkeypatch.setenv("PUBLISHED_CATALOG_GITEA_URL", "http://gitea.test")
monkeypatch.setenv("PUBLISHED_CATALOG_GITEA_TOKEN", "token-value")
monkeypatch.setenv("PUBLISHED_CATALOG_GITEA_REPO", "o/r")
class _BoomClient(_FakeClient):
def __init__(self, **kwargs) -> None:
super().__init__(_FakeResponse(404), _FakeResponse(201, {}))
def get(self, url, params=None):
raise httpx.ConnectError("boom")
monkeypatch.setattr(module.httpx, "Client", lambda **kwargs: _BoomClient(**kwargs))
code = main(["--catalog", str(catalog_file), "--baseline-set", "ss-prod-visual", "--version", "1"])
assert code == 1
assert "PUBLISH_UNAVAILABLE" in capsys.readouterr().out
# #endregion Test.Tooling.PublishCatalog.Main
# #endregion Test.Tooling.PublishCatalog