diff --git a/src/specify_cli/presets/_commands.py b/src/specify_cli/presets/_commands.py index b7e5ad06e5..17b846d68a 100644 --- a/src/specify_cli/presets/_commands.py +++ b/src/specify_cli/presets/_commands.py @@ -757,11 +757,16 @@ def preset_catalog_add( # Load existing config if config_path.exists(): try: - config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} + config = yaml.safe_load(config_path.read_text(encoding="utf-8")) except Exception as e: config_label = _display_project_path(project_root, config_path) console.print(f"[red]Error:[/red] Failed to read {_escape_markup(str(config_label))}: {_escape_markup(str(e))}") raise typer.Exit(1) + if config is None: + config = {} + elif not isinstance(config, dict): + console.print("[red]Error:[/red] Invalid catalog config: expected a mapping.") + raise typer.Exit(1) else: config = {} @@ -817,10 +822,15 @@ def preset_catalog_remove( raise typer.Exit(1) try: - config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} + config = yaml.safe_load(config_path.read_text(encoding="utf-8")) except Exception as e: console.print(f"[red]Error:[/red] Failed to read preset catalog config: {e}") raise typer.Exit(1) + if config is None: + config = {} + elif not isinstance(config, dict): + console.print("[red]Error:[/red] Invalid catalog config: expected a mapping.") + raise typer.Exit(1) catalogs = config.get("catalogs", []) if not isinstance(catalogs, list): diff --git a/tests/test_presets.py b/tests/test_presets.py index 317a1b437c..08ae8e9fb5 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -3274,6 +3274,38 @@ def test_catalog_remove_escapes_markup_in_not_found_error(self, project_dir): assert result.exit_code == 1 assert "[/red]absent" in result.output + @pytest.mark.parametrize( + "args", + [ + [ + "preset", + "catalog", + "add", + "https://example.com/catalog.json", + "--name", + "example", + ], + ["preset", "catalog", "remove", "example"], + ], + ) + def test_catalog_mutation_rejects_non_mapping_config_root( + self, project_dir, args + ): + from typer.testing import CliRunner + from unittest.mock import patch + from specify_cli import app + + config_path = project_dir / ".specify" / "preset-catalogs.yml" + original = "[]\n" + config_path.write_text(original, encoding="utf-8") + + with patch.object(Path, "cwd", return_value=project_dir): + result = CliRunner().invoke(app, args) + + assert result.exit_code == 1 + assert "expected a mapping" in result.output + assert config_path.read_text(encoding="utf-8") == original + def test_env_var_overrides_catalogs(self, project_dir, monkeypatch): """Test that SPECKIT_PRESET_CATALOG_URL env var overrides defaults.""" monkeypatch.setenv(