diff --git a/src/specify_cli/presets.py b/src/specify_cli/presets.py index fd9d4745f1..c6e75ae790 100644 --- a/src/specify_cli/presets.py +++ b/src/specify_cli/presets.py @@ -1903,12 +1903,24 @@ def _load_catalog_config(self, config_path: Path) -> Optional[List[PresetCatalog if not url: continue self._validate_catalog_url(url) + raw_priority = item.get("priority", idx + 1) + # Reject bools explicitly: ``bool`` is a subclass of ``int`` so + # ``int(True)`` silently returns 1, which would let a YAML + # ``priority: true`` slip through as a valid priority of 1. The + # sibling integration-catalog reader in ``catalogs.py`` already + # guards this; mirror the check here so the three catalog + # validators stay consistent. + if isinstance(raw_priority, bool): + raise PresetValidationError( + f"Invalid priority for catalog '{item.get('name', idx + 1)}': " + f"expected integer, got {raw_priority!r}" + ) try: - priority = int(item.get("priority", idx + 1)) + priority = int(raw_priority) except (TypeError, ValueError): raise PresetValidationError( f"Invalid priority for catalog '{item.get('name', idx + 1)}': " - f"expected integer, got {item.get('priority')!r}" + f"expected integer, got {raw_priority!r}" ) raw_install = item.get("install_allowed", False) if isinstance(raw_install, str): diff --git a/tests/test_presets.py b/tests/test_presets.py index 8fa700fa77..f1c0e95f4e 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -1830,6 +1830,31 @@ def test_load_catalog_config_invalid_priority(self, project_dir): with pytest.raises(PresetValidationError, match="Invalid priority"): catalog._load_catalog_config(config_path) + def test_load_catalog_config_rejects_boolean_priority(self, project_dir): + """A YAML ``priority: true`` is a typo, not a request for priority 1. + + ``bool`` is a subclass of ``int`` in Python, so ``int(True)`` silently + returns ``1``. Without an explicit guard a malformed config like + ``priority: yes`` would be accepted as a valid priority of 1 and + silently change catalog ordering. The sibling integration-catalog + reader rejects this case (see ``catalogs.py``); the preset catalog + reader must stay consistent. + """ + config_path = project_dir / ".specify" / "preset-catalogs.yml" + config_path.write_text(yaml.dump({ + "catalogs": [ + { + "name": "bool-priority", + "url": "https://example.com/catalog.json", + "priority": True, + } + ] + })) + + catalog = PresetCatalog(project_dir) + with pytest.raises(PresetValidationError, match="Invalid priority|expected integer"): + catalog._load_catalog_config(config_path) + def test_load_catalog_config_install_allowed_string(self, project_dir): """Test that install_allowed accepts string values.""" config_path = project_dir / ".specify" / "preset-catalogs.yml"