Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/specify_cli/presets/catalog/command_remove.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,18 @@ def preset_catalog_remove(
"[red]Error:[/red] Invalid catalog config: 'catalogs' must be a list."
)
raise typer.Exit(1)
for index, entry in enumerate(catalogs):
if not isinstance(entry, dict):
console.print(
f"[red]Error:[/red] Invalid catalog entry at index {index}: "
f"expected a mapping, got {type(entry).__name__}"
)
raise typer.Exit(1)
# Rendering only — the raw name drives the comparison below.
safe_name = _escape_markup(str(name))

original_count = len(catalogs)
catalogs = [c for c in catalogs if isinstance(c, dict) and c.get("name") != name]
catalogs = [c for c in catalogs if c.get("name") != name]

if len(catalogs) == original_count:
console.print(f"[red]Error:[/red] Catalog '{safe_name}' not found.")
Expand Down
85 changes: 85 additions & 0 deletions tests/specify_cli/presets/catalog/test_command_remove.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,97 @@

from pathlib import Path

import pytest
import yaml


class TestPresetCatalogRemove:
"""Test multi-catalog support in PresetCatalog."""

@pytest.mark.parametrize(
"invalid_entry",
[
pytest.param("broken-entry", id="string"),
pytest.param(0, id="number"),
pytest.param(False, id="boolean"),
pytest.param(None, id="null"),
pytest.param([], id="list"),
],
)
@pytest.mark.parametrize("invalid_index", [0, 1], ids=["first", "last"])
@pytest.mark.parametrize("name", ["keep", "does-not-exist"])
def test_catalog_remove_rejects_malformed_entries_without_writing(
self, project_dir, invalid_entry, invalid_index, name
):
from unittest.mock import patch

from typer.testing import CliRunner

from specify_cli import app

entries = [{"name": "keep", "url": "https://example.com/c.json"}]
entries.insert(invalid_index, invalid_entry)
config_path = project_dir / ".specify" / "preset-catalogs.yml"
config_path.write_text(
"# Preserve the original document on validation failure.\n"
+ yaml.safe_dump({"catalogs": entries, "metadata": "keep me"}),
encoding="utf-8",
)
original = config_path.read_bytes()

with patch.object(Path, "cwd", return_value=project_dir):
result = CliRunner().invoke(app, ["preset", "catalog", "remove", name])

assert config_path.read_bytes() == original
assert result.exit_code == 1, result.output
assert f"Invalid catalog entry at index {invalid_index}" in result.output
assert "expected a mapping" in result.output
assert type(invalid_entry).__name__ in result.output
assert "Removed catalog" not in result.output

@pytest.mark.parametrize(
"names,target,expected_names,exit_code",
[
pytest.param(["keep", "target"], "target", ["keep"], 0, id="remove-one"),
pytest.param(["keep"], "missing", ["keep"], 1, id="missing-name"),
pytest.param([], "missing", [], 1, id="empty-catalogs"),
pytest.param(["target"], "target", [], 0, id="remove-last"),
],
)
def test_catalog_remove_preserves_valid_config_behavior(
self, project_dir, names, target, expected_names, exit_code
):
from unittest.mock import patch

from typer.testing import CliRunner

from specify_cli import app

entries = [
{"name": name, "url": f"https://example.com/{name}.json"}
for name in names
]
config_path = project_dir / ".specify" / "preset-catalogs.yml"
config_path.write_text(
yaml.safe_dump({"catalogs": entries, "metadata": "keep me"}),
encoding="utf-8",
)
original = config_path.read_bytes()

with patch.object(Path, "cwd", return_value=project_dir):
result = CliRunner().invoke(app, ["preset", "catalog", "remove", target])

assert result.exit_code == exit_code, result.output
if exit_code:
assert config_path.read_bytes() == original
assert "Removed catalog" not in result.output
else:
config = yaml.safe_load(config_path.read_text(encoding="utf-8"))
assert config["catalogs"] == [
entry for entry in entries if entry["name"] in expected_names
]
assert config["metadata"] == "keep me"

def test_catalog_remove_escapes_rich_markup(self, project_dir):
"""`preset catalog remove` must not parse the name as Rich markup."""
from unittest.mock import patch
Expand Down