From 825fab7ad4ed1802303f1a2cb2aea8785c462223 Mon Sep 17 00:00:00 2001 From: marcelsafin <179933638+marcelsafin@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:53:45 +0200 Subject: [PATCH] fix(presets): reject non-mapping catalog mutations Assisted-by: GitHub Copilot (model: gpt-5.6-sol, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/presets/_commands.py | 14 ++++++++++-- tests/test_presets.py | 32 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) 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(