Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog/+ifc2404.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Skip mandatory field validation during object loading when `object_template` is specified.
13 changes: 8 additions & 5 deletions infrahub_sdk/spec/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
1 change: 1 addition & 0 deletions infrahub_sdk/testing/schemas/animal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=[
Expand Down
60 changes: 60 additions & 0 deletions tests/integration/test_spec_object.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"
16 changes: 16 additions & 0 deletions tests/unit/sdk/spec/test_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 == []