From 983225f23dfe8fac9de4cee59b991059aa41dd97 Mon Sep 17 00:00:00 2001 From: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> Date: Thu, 12 Feb 2026 07:50:02 -0800 Subject: [PATCH] fix: require attribute and amount names to be unique Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> --- src/openjd/model/v2023_09/_model.py | 4 +- .../v2023_09/test_step_host_requirements.py | 70 ++++++++++++++++--- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/openjd/model/v2023_09/_model.py b/src/openjd/model/v2023_09/_model.py index 6ad0a86a..d947ab3d 100644 --- a/src/openjd/model/v2023_09/_model.py +++ b/src/openjd/model/v2023_09/_model.py @@ -2579,7 +2579,7 @@ def _validate_amounts( return v if len(v) == 0: raise ValueError("List must contain at least one element or not be defined.") - return v + return validate_unique_elements(v, item_value=lambda v: v.name.lower(), property="name") @field_validator("attributes") @classmethod @@ -2590,7 +2590,7 @@ def _validate_attributes( return v if len(v) == 0: raise ValueError("List must contain at least one element or not be defined.") - return v + return validate_unique_elements(v, item_value=lambda v: v.name.lower(), property="name") @model_validator(mode="after") def _validate(self) -> Self: diff --git a/test/openjd/model/v2023_09/test_step_host_requirements.py b/test/openjd/model/v2023_09/test_step_host_requirements.py index a80ba123..43f5b27d 100644 --- a/test/openjd/model/v2023_09/test_step_host_requirements.py +++ b/test/openjd/model/v2023_09/test_step_host_requirements.py @@ -457,24 +457,26 @@ def test_parse_success(self, data: dict[str, Any]) -> None: # no exception was raised. @pytest.mark.parametrize( - "data,error_count", + "data,error_count,error_loc", [ - pytest.param({}, 1, id="missing amounts & attributes"), - pytest.param({"unknown": "value"}, 1, id="unknown field"), - pytest.param({"amounts": []}, 1, id="too few amounts"), - pytest.param({"attributes": []}, 1, id="too few attributes"), + pytest.param({}, 1, (), id="missing amounts & attributes"), + pytest.param({"unknown": "value"}, 1, ("unknown",), id="unknown field"), + pytest.param({"amounts": []}, 1, ("amounts",), id="too few amounts"), + pytest.param({"attributes": []}, 1, ("attributes",), id="too few attributes"), pytest.param( {"amounts": [{"name": f"amount.mycap{i}", "min": 1} for i in range(0, 51)]}, 1, + (), id="too many as only amounts", ), pytest.param( { "attributes": [ - {"name": f"attr.mycap{i}|", "anyOf": ["foo"]} for i in range(0, 51) + {"name": f"attr.mycap{i}", "anyOf": ["foo"]} for i in range(0, 51) ] }, 1, + (), id="too many as only attributes", ), pytest.param( @@ -485,16 +487,66 @@ def test_parse_success(self, data: dict[str, Any]) -> None: ], }, 1, + (), id="too many as combination", ), + pytest.param( + { + "amounts": [ + {"name": "amount.mycap", "min": 1}, + {"name": "amount.mycap", "min": 2}, + ] + }, + 1, + ("amounts",), + id="duplicate amount names", + ), + pytest.param( + { + "amounts": [ + {"name": "amount.mycap", "min": 1}, + {"name": "amount.MYCAP", "min": 2}, + ] + }, + 1, + ("amounts",), + id="duplicate amount names case insensitive", + ), + pytest.param( + { + "attributes": [ + {"name": "attr.mycap", "anyOf": ["foo"]}, + {"name": "attr.mycap", "anyOf": ["bar"]}, + ] + }, + 1, + ("attributes",), + id="duplicate attribute names", + ), + pytest.param( + { + "attributes": [ + {"name": "attr.mycap", "anyOf": ["foo"]}, + {"name": "attr.MYCAP", "anyOf": ["bar"]}, + ] + }, + 1, + ("attributes",), + id="duplicate attribute names case insensitive", + ), ], ) - def test_parse_fails(self, data: dict[str, Any], error_count: int) -> None: - # Failure case testing for Open Job Description AmountRequirementTemplate. + def test_parse_fails(self, data: dict[str, Any], error_count: int, error_loc: tuple) -> None: + # Failure case testing for Open Job Description HostRequirementsTemplate. # WHEN with pytest.raises(ValidationError) as excinfo: - _parse_model(model=AmountRequirementTemplate, obj=data) + _parse_model(model=HostRequirementsTemplate, obj=data) # THEN assert len(excinfo.value.errors()) == error_count, str(excinfo.value) + if error_loc: + actual_loc = excinfo.value.errors()[0]["loc"] + assert ( + actual_loc[: len(error_loc)] == error_loc + ), f"Expected {error_loc}, got {actual_loc}"