diff --git a/ods/extensions/services/dashboard-api/config.py b/ods/extensions/services/dashboard-api/config.py index b98c922fa..a49b57c12 100644 --- a/ods/extensions/services/dashboard-api/config.py +++ b/ods/extensions/services/dashboard-api/config.py @@ -565,8 +565,19 @@ def load_extension_catalog() -> list[dict]: return [] try: data = json.loads(CATALOG_PATH.read_text(encoding="utf-8")) - return data.get("extensions", []) - except (json.JSONDecodeError, OSError) as e: + if not isinstance(data, dict): + raise ValueError("catalog root must be an object") + extensions = data.get("extensions", []) + if not isinstance(extensions, list): + raise ValueError("catalog extensions must be a list") + valid_extensions = [item for item in extensions if isinstance(item, dict)] + if len(valid_extensions) != len(extensions): + logger.warning( + "Skipped %d non-object extension catalog entries", + len(extensions) - len(valid_extensions), + ) + return valid_extensions + except (json.JSONDecodeError, OSError, ValueError) as e: logger.warning("Failed to load extensions catalog: %s", e) return [] diff --git a/ods/extensions/services/dashboard-api/tests/test_config.py b/ods/extensions/services/dashboard-api/tests/test_config.py index a588aee36..e1e0adfe3 100644 --- a/ods/extensions/services/dashboard-api/tests/test_config.py +++ b/ods/extensions/services/dashboard-api/tests/test_config.py @@ -1,5 +1,6 @@ """Tests for config.py — manifest loading and service discovery.""" +import json import logging from pathlib import Path @@ -80,6 +81,27 @@ def test_live_env_value_strips_one_pair_and_preserves_unmatched_quotes(monkeypat assert config.read_live_env_value("PAIRED") == "model-v2" assert config.read_live_env_value("UNMATCHED") == "model-v2'" assert config.read_live_env_value("REPEATED") == "'model-v2'" +@pytest.mark.parametrize("payload", [[], {"extensions": {}}, "invalid"]) +def test_extension_catalog_rejects_invalid_payload_shape( + monkeypatch, tmp_path, payload +): + catalog = tmp_path / "extensions-catalog.json" + catalog.write_text(json.dumps(payload), encoding="utf-8") + monkeypatch.setattr(config, "CATALOG_PATH", catalog) + + assert config.load_extension_catalog() == [] + + +def test_extension_catalog_skips_non_object_entries(monkeypatch, tmp_path): + catalog = tmp_path / "extensions-catalog.json" + valid = {"id": "qdrant", "name": "Qdrant"} + catalog.write_text( + json.dumps({"extensions": [None, "invalid", valid]}), + encoding="utf-8", + ) + monkeypatch.setattr(config, "CATALOG_PATH", catalog) + + assert config.load_extension_catalog() == [valid] class TestReadManifestFile: