reuse-surface/tests/test_federation.py
tegwick 0c6b1e2538 Harden federated compose against malformed member indexes (REUSE-WP-0020)
Repointing the production hub's 50 Gitea-hosted federation sources to Forgejo
ahead of the 2026-08-31 CoulombCore retirement took /v1/federated to HTTP 500.
One member index (evidence-binder) has capability rows with no `id`, and
compose_federated_index dereferenced item["id"] unguarded. Its Gitea copy was a
stale snapshot returning a non-mapping, so those rows had never been parsed.

A single malformed member index must not take down the whole endpoint. Extract
_read_index_entries(): unparseable YAML, a non-mapping body, an empty file, and
a non-list `capabilities` each degrade to a warning and an empty row list, and
rows without an `id` are skipped individually. A failed source stays listed with
count 0 so it remains visible to operators rather than silently disappearing.

Also fix wall-clock rot in tests/test_plan_check.py, which was already failing
at clean HEAD: three tests pinned the compose date to a literal that has now
aged past STALE_DAYS.

Add workplan REUSE-WP-0020 covering the full cutover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 00:04:39 +02:00

285 lines
8.5 KiB
Python

from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import urllib.error
import yaml
from reuse_surface.federation import (
compose_federated_index,
fetch_remote_index_text,
load_federation_manifest,
resolve_source_index_path,
)
ROOT = Path(__file__).resolve().parent.parent
REMOTE_INDEX = """
version: 1
domain: helix_forge
updated: "2026-06-15"
capabilities:
- id: capability.remote.sample
name: Remote Sample
domain: helix_forge
vector: D2/A0/C0/R0
owner: example
path: registry/capabilities/capability.remote.sample.md
summary: Sample capability from a remote index
tags: [sample]
consumption_modes: [planning]
"""
def _remote_manifest() -> dict:
return {
"version": 1,
"domain": "helix_forge",
"collision_policy": "warn",
"sources": [
{
"repo": "local",
"index": "registry/indexes/capabilities.yaml",
"enabled": True,
"required": True,
},
{
"repo": "remote-repo",
"url": "https://example.com/capabilities.yaml",
"enabled": True,
"required": False,
"cache_ttl_seconds": 3600,
},
],
}
def test_manifest_with_url_source_validates():
manifest = _remote_manifest()
schema_path = ROOT / "schemas" / "federation.schema.yaml"
from jsonschema import Draft202012Validator
schema = yaml.safe_load(schema_path.read_text(encoding="utf-8"))
errors = list(Draft202012Validator(schema).iter_errors(manifest))
assert errors == []
def test_fetch_remote_index_text():
source = {"repo": "remote-repo", "url": "https://example.com/capabilities.yaml"}
payload = REMOTE_INDEX.encode("utf-8")
class FakeResponse:
def __enter__(self):
return self
def __exit__(self, *args):
return False
def read(self):
return payload
with patch("urllib.request.urlopen", return_value=FakeResponse()):
text = fetch_remote_index_text(source["url"], source)
assert "capability.remote.sample" in text
def test_remote_fetch_writes_cache(tmp_path, monkeypatch):
monkeypatch.setattr("reuse_surface.federation.CACHE_DIR", tmp_path / "cache")
source = {
"repo": "remote-repo",
"url": "https://example.com/capabilities.yaml",
"cache_ttl_seconds": 3600,
}
payload = REMOTE_INDEX.encode("utf-8")
class FakeResponse:
def __enter__(self):
return self
def __exit__(self, *args):
return False
def read(self):
return payload
with patch("urllib.request.urlopen", return_value=FakeResponse()):
path, warnings = resolve_source_index_path(source)
assert path is not None
assert warnings == []
assert path.exists()
assert "capability.remote.sample" in path.read_text(encoding="utf-8")
def test_remote_fetch_uses_fresh_cache_without_refetch(tmp_path, monkeypatch):
monkeypatch.setattr("reuse_surface.federation.CACHE_DIR", tmp_path / "cache")
source = {
"repo": "remote-repo",
"url": "https://example.com/capabilities.yaml",
"cache_ttl_seconds": 3600,
}
payload = REMOTE_INDEX.encode("utf-8")
class FakeResponse:
def __enter__(self):
return self
def __exit__(self, *args):
return False
def read(self):
return payload
with patch("urllib.request.urlopen", return_value=FakeResponse()) as urlopen:
resolve_source_index_path(source)
path, warnings = resolve_source_index_path(source)
assert warnings == []
assert path is not None
urlopen.assert_called_once()
def test_remote_fetch_falls_back_to_stale_cache(tmp_path, monkeypatch):
monkeypatch.setattr("reuse_surface.federation.CACHE_DIR", tmp_path / "cache")
source = {
"repo": "remote-repo",
"url": "https://example.com/capabilities.yaml",
"cache_ttl_seconds": 0,
}
payload = REMOTE_INDEX.encode("utf-8")
class FakeResponse:
def __enter__(self):
return self
def __exit__(self, *args):
return False
def read(self):
return payload
with patch("urllib.request.urlopen", return_value=FakeResponse()):
resolve_source_index_path(source)
with patch(
"urllib.request.urlopen",
side_effect=urllib.error.URLError("network down"),
):
path, warnings = resolve_source_index_path(source)
assert path is not None
assert any("stale cache" in warning for warning in warnings)
def test_compose_merges_remote_capabilities(tmp_path, monkeypatch):
monkeypatch.setattr("reuse_surface.federation.CACHE_DIR", tmp_path / "cache")
source = {
"repo": "remote-repo",
"url": "https://example.com/capabilities.yaml",
"enabled": True,
"cache_ttl_seconds": 3600,
}
payload = REMOTE_INDEX.encode("utf-8")
class FakeResponse:
def __enter__(self):
return self
def __exit__(self, *args):
return False
def read(self):
return payload
manifest = _remote_manifest()
with patch("urllib.request.urlopen", return_value=FakeResponse()):
federated, warnings = compose_federated_index(manifest)
ids = {item["id"] for item in federated["capabilities"]}
assert "capability.remote.sample" in ids
assert "capability.registry.register" in ids
remote = next(
item for item in federated["capabilities"] if item["id"] == "capability.remote.sample"
)
assert remote["source_repo"] == "remote-repo"
assert remote["source_url"] == "https://example.com/capabilities.yaml"
def test_load_production_manifest_still_validates():
manifest = load_federation_manifest()
assert manifest["domain"] == "helix_forge"
assert any(source["repo"] == "reuse-surface" for source in manifest["sources"])
def _compose_with_remote_body(body: str, tmp_path, monkeypatch):
"""Compose with the remote member index serving `body` verbatim."""
monkeypatch.setattr("reuse_surface.federation.CACHE_DIR", tmp_path / "cache")
payload = body.encode("utf-8")
class FakeResponse:
def __enter__(self):
return self
def __exit__(self, *args):
return False
def read(self):
return payload
with patch("urllib.request.urlopen", return_value=FakeResponse()):
return compose_federated_index(_remote_manifest())
def _local_ids_survived(federated) -> bool:
return any(item["source_repo"] == "local" for item in federated["capabilities"])
def test_compose_skips_capability_without_id(tmp_path, monkeypatch):
body = """
version: 1
domain: helix_forge
capabilities:
- type: library
title: No id at all
- id: capability.remote.sample
name: Remote Sample
"""
federated, warnings = _compose_with_remote_body(body, tmp_path, monkeypatch)
ids = {item["id"] for item in federated["capabilities"]}
assert "capability.remote.sample" in ids
assert _local_ids_survived(federated)
assert any("remote-repo" in w and "no id" in w for w in warnings)
def test_compose_skips_non_mapping_index(tmp_path, monkeypatch):
federated, warnings = _compose_with_remote_body(
"just a string, not a mapping\n", tmp_path, monkeypatch
)
assert _local_ids_survived(federated)
assert any("not a mapping" in w for w in warnings)
def test_compose_skips_empty_index(tmp_path, monkeypatch):
federated, warnings = _compose_with_remote_body("", tmp_path, monkeypatch)
assert _local_ids_survived(federated)
assert any("remote-repo" in w for w in warnings)
def test_compose_skips_unparseable_index(tmp_path, monkeypatch):
federated, warnings = _compose_with_remote_body(
"capabilities: [unclosed\n", tmp_path, monkeypatch
)
assert _local_ids_survived(federated)
assert any("unparseable" in w for w in warnings)
def test_compose_skips_capabilities_not_a_list(tmp_path, monkeypatch):
federated, warnings = _compose_with_remote_body(
"version: 1\ncapabilities: not-a-list\n", tmp_path, monkeypatch
)
assert _local_ids_survived(federated)
assert any("not a list" in w for w in warnings)
def test_malformed_source_still_listed_with_zero_count(tmp_path, monkeypatch):
federated, _ = _compose_with_remote_body(
"just a string, not a mapping\n", tmp_path, monkeypatch
)
remote = next(s for s in federated["sources"] if s["repo"] == "remote-repo")
assert remote["count"] == 0