Skip to content

Commit 0f362d3

Browse files
jawwad-aliclaude
andcommitted
test(integrations): unit-test the shared catalog-shape validator directly
Replace the integration-level missing-schema_version cache test (which was masked by multi-source merging — a sibling catalog source still supplied the entry, so it passed regardless of the fix) with a direct unit test of _catalog_shape_error. This deterministically proves both paths now reject a payload missing schema_version, a non-dict integrations, or a non-dict payload, and accept a well-formed one. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b88bce9 commit 0f362d3

1 file changed

Lines changed: 25 additions & 41 deletions

File tree

tests/integrations/test_integration_catalog.py

Lines changed: 25 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,34 @@
1313
IntegrationDescriptor,
1414
IntegrationDescriptorError,
1515
IntegrationValidationError,
16+
_catalog_shape_error,
1617
)
1718

1819

20+
class TestCatalogShapeValidator:
21+
"""The shared shape validator used by BOTH the fresh-fetch and cache-read
22+
paths, so a poisoned/older cache can't bypass the format contract the fresh
23+
fetch enforces (dict + 'schema_version' + dict 'integrations')."""
24+
25+
def test_valid_payload_returns_none(self):
26+
assert _catalog_shape_error({"schema_version": "1.0", "integrations": {}}) is None
27+
28+
def test_missing_schema_version_is_rejected(self):
29+
# The exact bypass the two paths used to disagree on: a dict with a dict
30+
# 'integrations' but no 'schema_version'.
31+
assert _catalog_shape_error({"integrations": {}}) is not None
32+
33+
def test_missing_integrations_is_rejected(self):
34+
assert _catalog_shape_error({"schema_version": "1.0"}) is not None
35+
36+
def test_non_dict_integrations_is_rejected(self):
37+
assert _catalog_shape_error({"schema_version": "1.0", "integrations": []}) is not None
38+
39+
@pytest.mark.parametrize("payload", [[], "x", 5, None])
40+
def test_non_dict_payload_is_rejected(self, payload):
41+
assert _catalog_shape_error(payload) is not None
42+
43+
1944
# ---------------------------------------------------------------------------
2045
# IntegrationCatalogEntry
2146
# ---------------------------------------------------------------------------
@@ -293,47 +318,6 @@ def test_poisoned_cache_shape_is_dropped_and_refetched(self, tmp_path, monkeypat
293318
results = cat.search()
294319
assert "acme-coder" in [r["id"] for r in results]
295320

296-
def test_cache_missing_schema_version_is_dropped_and_refetched(self, tmp_path, monkeypatch):
297-
"""A cache that has a dict 'integrations' but no 'schema_version' must be
298-
dropped and refetched — the cache path enforces the SAME contract as a
299-
fresh fetch (which rejects a missing schema_version). Otherwise an
300-
older/poisoned payload like {"integrations": {}} bypasses the format
301-
contract and hides real catalog entries."""
302-
monkeypatch.setenv("HOME", str(tmp_path))
303-
monkeypatch.setenv("USERPROFILE", str(tmp_path))
304-
monkeypatch.delenv("SPECKIT_INTEGRATION_CATALOG_URL", raising=False)
305-
(tmp_path / ".specify").mkdir()
306-
cat = IntegrationCatalog(tmp_path)
307-
308-
catalog = {
309-
"schema_version": "1.0",
310-
"updated_at": "2026-01-01T00:00:00Z",
311-
"integrations": {
312-
"acme-coder": {
313-
"id": "acme-coder", "name": "Acme Coder", "version": "2.0.0",
314-
"description": "Community integration", "author": "acme-org",
315-
"tags": ["cli"],
316-
},
317-
},
318-
}
319-
self._patch_urlopen(monkeypatch, catalog)
320-
cat.search() # populate the cache legitimately
321-
322-
cache_dir = tmp_path / ".specify" / "integrations" / ".cache"
323-
data_files = [
324-
f for f in cache_dir.glob("catalog-*.json")
325-
if not f.name.endswith("-metadata.json")
326-
]
327-
assert data_files, "cache was not populated"
328-
# Well-shaped except for the missing schema_version key.
329-
data_files[0].write_text(
330-
json.dumps({"integrations": {}}),
331-
encoding="utf-8",
332-
)
333-
334-
results = cat.search()
335-
assert "acme-coder" in [r["id"] for r in results]
336-
337321
def test_search_by_tag(self, tmp_path, monkeypatch):
338322
monkeypatch.setenv("HOME", str(tmp_path))
339323
monkeypatch.setenv("USERPROFILE", str(tmp_path))

0 commit comments

Comments
 (0)