Skip to content

Commit 68c5d76

Browse files
authored
Merge pull request #4414 from opsmill/dga-20240921-fix-4325
Add validation for hierarchy parent and children
2 parents 5ef943c + 0e4cb63 commit 68c5d76

File tree

5 files changed

+104
-0
lines changed

5 files changed

+104
-0
lines changed

backend/infrahub/core/relationship/model.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,8 @@ async def init(
734734
if not isinstance(data, list):
735735
data = [data]
736736

737+
await rm._validate_hierarchy()
738+
737739
for item in data:
738740
if not isinstance(item, (rm.rel_class, str, dict)) and not hasattr(item, "_schema"):
739741
raise ValidationError({rm.name: f"Invalid data provided to form a relationship {item}"})
@@ -966,6 +968,8 @@ async def update( # pylint: disable=too-many-branches
966968
else:
967969
list_data = data
968970

971+
await self._validate_hierarchy()
972+
969973
# Reset the list of relationship and save the previous one to see if we can reuse some
970974
previous_relationships = {rel.peer_id: rel for rel in await self.get_relationships(db=db) if rel.peer_id}
971975
self._relationships.clear()
@@ -1023,6 +1027,8 @@ async def add(self, data: Union[dict[str, Any], Node], db: InfrahubDatabase) ->
10231027
if not isinstance(data, (self.rel_class, dict)) and not hasattr(data, "_schema"):
10241028
raise ValidationError({self.name: f"Invalid data provided to form a relationship {data}"})
10251029

1030+
await self._validate_hierarchy()
1031+
10261032
previous_relationships = {rel.peer_id for rel in await self.get_relationships(db=db) if rel.peer_id}
10271033

10281034
item_id = getattr(data, "id", None)
@@ -1148,3 +1154,14 @@ async def to_graphql(
11481154
return None
11491155

11501156
return await relationships[0].to_graphql(fields=fields, db=db, related_node_ids=related_node_ids)
1157+
1158+
async def _validate_hierarchy(self) -> None:
1159+
schema = self.node.get_schema()
1160+
if schema.is_profile_schema or not schema.hierarchy: # type: ignore[union-attr]
1161+
return
1162+
1163+
if self.name == "parent" and not schema.parent: # type: ignore[union-attr]
1164+
raise ValidationError({self.name: f"Not supported to assign a value to parent for {schema.kind}"})
1165+
1166+
if self.name == "children" and not schema.children: # type: ignore[union-attr]
1167+
raise ValidationError({self.name: f"Not supported to assign some children for {schema.kind}"})

backend/tests/unit/conftest.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from infrahub.core.node.resource_manager.ip_address_pool import CoreIPAddressPool
3636
from infrahub.core.node.resource_manager.ip_prefix_pool import CoreIPPrefixPool
3737
from infrahub.core.protocols_base import CoreNode
38+
from infrahub.core.relationship import RelationshipManager
3839
from infrahub.core.schema import (
3940
GenericSchema,
4041
NodeSchema,
@@ -2062,6 +2063,42 @@ async def hierarchical_location_schema_simple(db: InfrahubDatabase, default_bran
20622063
registry.schema.register_schema(schema=schema, branch=default_branch.name)
20632064

20642065

2066+
@pytest.fixture
2067+
async def location_generic_protocol():
2068+
class LocationGeneric(CoreNode):
2069+
name: String
2070+
status: StringOptional
2071+
things: RelationshipManager
2072+
parent: RelationshipManager
2073+
children: RelationshipManager
2074+
2075+
return LocationGeneric
2076+
2077+
2078+
@pytest.fixture
2079+
async def location_site_protocol(location_generic_protocol):
2080+
class LocationSite(location_generic_protocol):
2081+
pass
2082+
2083+
return LocationSite
2084+
2085+
2086+
@pytest.fixture
2087+
async def location_region_protocol(location_generic_protocol):
2088+
class LocationRegion(location_generic_protocol):
2089+
pass
2090+
2091+
return LocationRegion
2092+
2093+
2094+
@pytest.fixture
2095+
async def location_rack_protocol(location_generic_protocol):
2096+
class LocationRack(location_generic_protocol):
2097+
pass
2098+
2099+
return LocationRack
2100+
2101+
20652102
@pytest.fixture
20662103
async def hierarchical_location_schema(
20672104
db: InfrahubDatabase, default_branch: Branch, hierarchical_location_schema_simple, register_core_models_schema
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import pytest
2+
3+
from infrahub.core import registry
4+
from infrahub.core.manager import NodeManager
5+
from infrahub.core.node import Node
6+
from infrahub.database import InfrahubDatabase
7+
from infrahub.exceptions import ValidationError
8+
9+
10+
async def test_create_node_with_invalid_hierarchy(db: InfrahubDatabase, hierarchical_location_data):
11+
region_schema = registry.schema.get_node_schema(name="LocationRegion", duplicate=True)
12+
rack_schema = registry.schema.get_node_schema(name="LocationRack", duplicate=True)
13+
europe = await NodeManager.get_one(db=db, id=hierarchical_location_data["europe"].id)
14+
city = await NodeManager.get_one(db=db, id=hierarchical_location_data["seattle"].id)
15+
16+
with pytest.raises(ValidationError) as exc:
17+
region = await Node.init(db=db, schema=region_schema)
18+
await region.new(db=db, name="region2", parent=city)
19+
assert "Not supported to assign a value to parent" in str(exc.value)
20+
21+
with pytest.raises(ValidationError) as exc:
22+
rack = await Node.init(db=db, schema=rack_schema)
23+
await rack.new(db=db, name="rack2", parent=city, children=[europe])
24+
assert "Not supported to assign some children" in str(exc.value)

backend/tests/unit/core/hierarchy/test_hierarchy_update.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import pytest
2+
13
from infrahub.core import registry
24
from infrahub.core.manager import NodeManager
35
from infrahub.database import InfrahubDatabase
6+
from infrahub.exceptions import ValidationError
47

58
CHECK_HIERARCHY_QUERY = """
69
MATCH ({uuid: $node_uuid})-[rel:IS_RELATED]-(rel_node:Relationship {name: "parent__child"})
@@ -30,3 +33,25 @@ async def test_update_node_with_hierarchy(db: InfrahubDatabase, hierarchical_loc
3033
assert result.get("rel").get("hierarchy") == site_schema.hierarchy
3134
nodes = await NodeManager.query(db=db, schema=site_schema, filters={"parent__name__value": "europe"})
3235
assert {node.name.value for node in nodes} == {"paris", "london", "seattle"}
36+
37+
38+
async def test_update_node_invalid_hierarchy(db: InfrahubDatabase, hierarchical_location_data):
39+
city = await NodeManager.get_one(db=db, id=hierarchical_location_data["seattle"].id, raise_on_error=True)
40+
region = await NodeManager.get_one(db=db, id=hierarchical_location_data["europe"].id, raise_on_error=True)
41+
rack = await NodeManager.get_one(db=db, id=hierarchical_location_data["paris-r1"].id, raise_on_error=True)
42+
43+
with pytest.raises(ValidationError) as exc:
44+
await region.parent.update(db=db, data=city)
45+
assert "Not supported to assign a value to parent" in str(exc.value)
46+
47+
with pytest.raises(ValidationError) as exc:
48+
await region.parent.add(db=db, data=city)
49+
assert "Not supported to assign a value to parent" in str(exc.value)
50+
51+
with pytest.raises(ValidationError) as exc:
52+
await rack.children.update(db=db, data=[region])
53+
assert "Not supported to assign some children" in str(exc.value)
54+
55+
with pytest.raises(ValidationError) as exc:
56+
await rack.children.update(db=db, data=[region])
57+
assert "Not supported to assign some children" in str(exc.value)

changelog/4325.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hierarchical node that don't have a parent or a children defined in the schema will properly enforce that constraint

0 commit comments

Comments
 (0)