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
8 changes: 8 additions & 0 deletions src/specify_cli/bundler/models/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,17 @@ def structural_errors(self) -> list[str]:
"(lowercase letters, digits, '.', '_', '-'; no path separators)."
)

seen_components: set[tuple[str, str]] = set()
for ref in self.components:
if not ref.id:
errors.append(f"A {ref.kind[:-1]} entry is missing its 'id'.")
key = (ref.kind, ref.id)
if ref.id and key in seen_components:
errors.append(
f"Duplicate {ref.kind[:-1]} '{ref.id}' in "
f"'provides.{ref.kind}'."
)
seen_components.add(key)
if ref.kind != "steps" and not ref.version:
errors.append(
f"{ref.kind[:-1]} '{ref.id or '<unknown>'}' must be pinned to a 'version'."
Expand Down
23 changes: 23 additions & 0 deletions tests/contract/test_manifest_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,29 @@ def test_components_property_orders_by_kind():
assert kinds == ["extensions", "presets", "steps", "workflows"]


def test_duplicate_component_in_same_kind_is_rejected():
data = valid_manifest_dict()
data["provides"]["extensions"].append(
{"id": "ext-a", "version": "9.9.9"}
)

errors = BundleManifest.from_dict(data).structural_errors()

assert any(
"duplicate extension 'ext-a'" in error.lower()
for error in errors
)


def test_same_component_id_in_different_kinds_is_allowed():
data = valid_manifest_dict()
data["provides"]["steps"].append({"id": "ext-a"})

errors = BundleManifest.from_dict(data).structural_errors()

assert not any("duplicate" in error.lower() for error in errors)


def test_string_tags_rejected_not_split_per_character():
# A bare string would otherwise be iterated character-by-character; the
# schema requires a list of strings.
Expand Down