188 lines
7.5 KiB
Python
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
|