diff --git a/changelog/+ifc2404.fixed.md b/changelog/+ifc2404.fixed.md new file mode 100644 index 00000000..42b03784 --- /dev/null +++ b/changelog/+ifc2404.fixed.md @@ -0,0 +1 @@ +Skip mandatory field validation during object loading when `object_template` is specified. diff --git a/infrahub_sdk/spec/object.py b/infrahub_sdk/spec/object.py index 30d9f93d..4eb2b882 100644 --- a/infrahub_sdk/spec/object.py +++ b/infrahub_sdk/spec/object.py @@ -264,11 +264,14 @@ async def validate_object( context = context.copy() if context else {} # First validate if all mandatory fields are present - errors.extend( - ObjectValidationError(position=[*position, element], message=f"{element} is mandatory") - for element in schema.mandatory_input_names - if not any([element in data, element in context]) - ) + # Skip mandatory check when an object_template is specified, as the template provides defaults, we expect the API server to just send a + # response with a failure if the template is not valid or doesn't provide all mandatory fields + if "object_template" not in data: + errors.extend( + ObjectValidationError(position=[*position, element], message=f"{element} is mandatory") + for element in schema.mandatory_input_names + if not any([element in data, element in context]) + ) # Validate if all attributes are valid for key, value in data.items(): diff --git a/infrahub_sdk/testing/schemas/animal.py b/infrahub_sdk/testing/schemas/animal.py index 559e75d4..bbc9992b 100644 --- a/infrahub_sdk/testing/schemas/animal.py +++ b/infrahub_sdk/testing/schemas/animal.py @@ -66,6 +66,7 @@ def schema_dog(self) -> NodeSchema: name="Dog", namespace=NAMESPACE, include_in_menu=True, + generate_template=True, inherit_from=[TESTING_ANIMAL], display_labels=["name__value", "breed__value"], attributes=[ diff --git a/tests/integration/test_spec_object.py b/tests/integration/test_spec_object.py index c7320b38..c8e7eca3 100644 --- a/tests/integration/test_spec_object.py +++ b/tests/integration/test_spec_object.py @@ -1,8 +1,10 @@ from pathlib import Path +from typing import Any import pytest from infrahub_sdk import InfrahubClient +from infrahub_sdk.exceptions import GraphQLError from infrahub_sdk.schema import SchemaRoot from infrahub_sdk.spec.menu import MenuFile from infrahub_sdk.spec.object import ObjectFile @@ -148,3 +150,61 @@ async def test_load_menu(self, client: InfrahubClient, branch_name: str, initial await menu_by_name["Animals"].children.fetch() peer_labels = [peer.display_label for peer in menu_by_name["Animals"].children.peers] assert sorted(peer_labels) == sorted(["Dog", "Cat"]) + + +def make_object_file(kind: str, data: list[dict[str, Any]]) -> ObjectFile: + return ObjectFile( + location=Path("inline"), + content={"apiVersion": "infrahub.app/v1", "kind": "Object", "spec": {"kind": kind, "data": data}}, + ) + + +class TestSpecObjectWithTemplate(TestInfrahubDockerClient, SchemaAnimal): + @pytest.fixture(scope="class") + async def initial_schema(self, default_branch: str, client: InfrahubClient, schema_base: SchemaRoot) -> None: + await client.schema.wait_until_converged(branch=default_branch) + resp = await client.schema.load( + schemas=[schema_base.to_schema_dict()], branch=default_branch, wait_until_converged=True + ) + assert resp.errors == {} + + async def test_create_owner(self, client: InfrahubClient, initial_schema: None) -> None: + owner = await client.create(kind="TestingPerson", name="Template Owner") + await owner.save() + + async def test_create_templates(self, client: InfrahubClient, initial_schema: None) -> None: + complete = make_object_file( + kind="TemplateTestingDog", data=[{"template_name": "Complete Dog", "breed": "Labrador"}] + ) + await complete.validate_format(client=client) + await complete.process(client=client) + + incomplete = make_object_file(kind="TemplateTestingDog", data=[{"template_name": "Incomplete Dog"}]) + await incomplete.validate_format(client=client) + await incomplete.process(client=client) + + async def test_dog_with_complete_template(self, client: InfrahubClient, initial_schema: None) -> None: + """Mandatory field is provided by the template.""" + obj = make_object_file( + kind="TestingDog", + data=[{"name": "Rex", "owner": "Template Owner", "object_template": "Complete Dog"}], + ) + await obj.validate_format(client=client) + await obj.process(client=client) + + dog = await client.get(kind="TestingDog", name__value="Rex") + assert dog.breed.value == "Labrador" + + async def test_dog_with_incomplete_template_fails(self, client: InfrahubClient, initial_schema: None) -> None: + """Mandatory field is not provided by the template.""" + obj = make_object_file( + kind="TestingDog", + data=[{"name": "Buddy", "owner": "Template Owner", "object_template": "Incomplete Dog"}], + ) + await obj.validate_format(client=client) + + with pytest.raises(GraphQLError) as exc_info: + await obj.process(client=client) + + assert len(exc_info.value.errors) == 1 + assert exc_info.value.errors[0]["message"] == "breed is mandatory for TestingDog at breed" diff --git a/tests/unit/sdk/spec/test_object.py b/tests/unit/sdk/spec/test_object.py index edb06891..fda035c4 100644 --- a/tests/unit/sdk/spec/test_object.py +++ b/tests/unit/sdk/spec/test_object.py @@ -9,6 +9,7 @@ from infrahub_sdk.exceptions import ValidationError from infrahub_sdk.node.related_node import RelatedNode from infrahub_sdk.spec.object import ( + InfrahubObjectFileData, ObjectFile, RelationshipDataFormat, get_relationship_info, @@ -456,3 +457,18 @@ def test_related_node_graphql_payload(test_case: RelatedNodePayloadTestCase) -> # Verify the payload structure assert payload == test_case.expected_payload, f"Expected payload {test_case.expected_payload}, got {payload}" + + +async def test_validate_object_skips_mandatory_check_with_object_template( + client_with_schema_01: InfrahubClient, +) -> None: + """When object_template is present, mandatory field validation should be skipped.""" + schema = await client_with_schema_01.schema.get(kind="BuiltinLocation") + + data = {"name": "Site1", "object_template": "Standard Site"} + errors = await InfrahubObjectFileData.validate_object( + client=client_with_schema_01, position=[1], schema=schema, data=data + ) + + mandatory_errors = [e for e in errors if e.message == "type is mandatory"] + assert mandatory_errors == []