Skip to content
Merged
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ jobs:
- name: "Check out repository code"
uses: "actions/checkout@v6"
- name: "Linting: markdownlint"
uses: DavidAnson/markdownlint-cli2-action@v22
uses: DavidAnson/markdownlint-cli2-action@v23
with:
config: docs/.markdownlint.yaml
globs: |
Expand Down Expand Up @@ -175,7 +175,7 @@ jobs:
- name: Install NodeJS
uses: actions/setup-node@v5
with:
node-version: 20
node-version: 24
cache: "npm"
cache-dependency-path: docs/package-lock.json
- name: "Install dependencies"
Expand Down Expand Up @@ -210,7 +210,7 @@ jobs:
- name: Install NodeJS
uses: actions/setup-node@v5
with:
node-version: 20
node-version: 24
cache: "npm"
cache-dependency-path: docs/package-lock.json
- name: "Install npm dependencies"
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/repository-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ jobs:
matrix:
# Either a literal path, or the name of a secret...
repo:
- "opsmill/infrahub-bundle-dc"
- "opsmill/emma"
- "opsmill/infrahub-demo-dc"
- "INFRAHUB_CUSTOMER1_REPOSITORY"
- "INFRAHUB_CUSTOMER3_REPOSITORY"

Expand Down
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.
1 change: 1 addition & 0 deletions changelog/891.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Corrected protocol typing for IPHost.value IPAddress -> IPInterface
4 changes: 2 additions & 2 deletions infrahub_sdk/protocols_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,11 @@ class IntegerOptional(Attribute):


class IPHost(Attribute):
value: ipaddress.IPv4Address | ipaddress.IPv6Address
value: ipaddress.IPv4Interface | ipaddress.IPv6Interface


class IPHostOptional(Attribute):
value: ipaddress.IPv4Address | ipaddress.IPv6Address | None
value: ipaddress.IPv4Interface | ipaddress.IPv6Interface | None


class IPNetwork(Attribute):
Expand Down
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 == []