From d2441d32c340c6498ea46b4d8b2cc9453e972c0a Mon Sep 17 00:00:00 2001 From: polmichel Date: Thu, 5 Feb 2026 15:09:04 +0100 Subject: [PATCH 1/3] feat: update the generic schema on python_sdk side regarding restricted_namespaces but also missing fields IHS-190 --- infrahub_sdk/schema/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/infrahub_sdk/schema/main.py b/infrahub_sdk/schema/main.py index 34a35177..28fa28e4 100644 --- a/infrahub_sdk/schema/main.py +++ b/infrahub_sdk/schema/main.py @@ -289,7 +289,9 @@ class GenericSchemaAPI(BaseSchema, BaseSchemaAttrRelAPI): """A Generic can be either an Interface or a Union depending if there are some Attributes or Relationships defined.""" hash: str | None = None + hierarchical: bool | None = None used_by: list[str] = Field(default_factory=list) + restricted_namespaces: list[str] | None = None class BaseNodeSchema(BaseSchema): From 4ed7fe2c1be955c5ca12022a045944a59d6f06ec Mon Sep 17 00:00:00 2001 From: polmichel Date: Sun, 8 Feb 2026 19:53:23 +0100 Subject: [PATCH 2/3] test: New generic test on sdk module. Loading valid generic schema through infrahubctl command layer. Infrahub API is mocked IHS-190 --- .../fixtures/models/valid_generic_schema.json | 30 +++++++++++++ tests/unit/ctl/test_schema_app.py | 45 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 tests/fixtures/models/valid_generic_schema.json diff --git a/tests/fixtures/models/valid_generic_schema.json b/tests/fixtures/models/valid_generic_schema.json new file mode 100644 index 00000000..82b1d0be --- /dev/null +++ b/tests/fixtures/models/valid_generic_schema.json @@ -0,0 +1,30 @@ +{ + "version": "1.0", + "generics": [ + { + "name": "Animal", + "namespace": "Testing", + "attributes": [ + { + "name": "name", + "kind": "Text" + } + ], + "restricted_namespaces": ["Dog"] + } + ], + "nodes": [ + { + "name": "Dog", + "namespace": "Dog", + "inherit_from": ["TestingAnimal"], + "attributes": [ + { + "name": "breed", + "kind": "Text", + "optional": true + } + ] + } + ] +} diff --git a/tests/unit/ctl/test_schema_app.py b/tests/unit/ctl/test_schema_app.py index 1fcfe62b..11fe51a2 100644 --- a/tests/unit/ctl/test_schema_app.py +++ b/tests/unit/ctl/test_schema_app.py @@ -128,3 +128,48 @@ def test_schema_load_notvalid_namespace(httpx_mock: HTTPXMock) -> None: fixture_file.read_text(encoding="utf-8"), ) assert content_json == {"schemas": [fixture_file_content]} + + +def test_load_valid_generic_schema(httpx_mock: HTTPXMock) -> None: + """A test which ensures that a generic schema is correctly loaded when loaded from infrahubctl command""" + + # Arrange + fixture_file = get_fixtures_dir() / "models" / "valid_generic_schema.json" + + httpx_mock.add_response( + method="POST", + url="http://mock/api/schema/load?branch=main", + status_code=200, + json={ + "hash": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", + "previous_hash": "d3f7f4e7161f0ae6538a01d5a42dc661", + "diff": { + "added": { + "TestingAnimal": {"added": {}, "changed": {}, "removed": {}}, + "DogDog": {"added": {}, "changed": {}, "removed": {}}, + }, + "changed": {}, + "removed": {}, + }, + "schema_updated": True, + }, + ) + + # Act + result = runner.invoke(app=app, args=["load", str(fixture_file)]) + + # Assert + assert result.exit_code == 0 + assert f"schema '{fixture_file}' loaded successfully" in remove_ansi_color(result.stdout.replace("\n", "")) + + content = httpx_mock.get_requests()[0].content.decode("utf8") + content_json = yaml.safe_load(content) + fixture_file_content = yaml.safe_load( + fixture_file.read_text(encoding="utf-8"), + ) + assert content_json == {"schemas": [fixture_file_content]} + + # Verify restricted_namespaces is present in the payload sent to the API + sent_generics = content_json["schemas"][0]["generics"] + assert len(sent_generics) == 1 + assert sent_generics[0]["restricted_namespaces"] == ["Dog"] From 108352530dda87ce24a190b0f3811d61dcfa8c0d Mon Sep 17 00:00:00 2001 From: polmichel Date: Sun, 8 Feb 2026 20:07:53 +0100 Subject: [PATCH 3/3] test: new test on sdk module. An error message is retrieved when an invalid namespace is loaded from SDK methods. Infrahub API is mocked IHS-190 --- tests/unit/sdk/test_schema.py | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/unit/sdk/test_schema.py b/tests/unit/sdk/test_schema.py index 05302b11..5772850c 100644 --- a/tests/unit/sdk/test_schema.py +++ b/tests/unit/sdk/test_schema.py @@ -1,4 +1,5 @@ import inspect +import re from io import StringIO from unittest import mock from unittest.mock import MagicMock @@ -482,3 +483,56 @@ def test_schema_base__get_schema_name__returns_correct_schema_name_for_protocols assert InfrahubSchemaBase._get_schema_name(schema=BuiltinIPAddressSync) == "BuiltinIPAddress" assert InfrahubSchemaBase._get_schema_name(schema=BuiltinIPAddress) == "BuiltinIPAddress" assert InfrahubSchemaBase._get_schema_name(schema="BuiltinIPAddress") == "BuiltinIPAddress" + + +async def test_schema_load_rejected_when_node_namespace_violates_generic_restricted_namespaces( + client: InfrahubClient, httpx_mock: HTTPXMock +) -> None: + """Validate that loading a schema with a node whose namespace violates the generic's restricted_namespaces + is rejected. One test is already testing the API internal behavior + tests.integration.schema_lifecycle.test_restricted_namespaces_validation. + TestRestrictedNamespacesValidation.test_change_restriction_should_fail""" + # Arrange + schema_payload = { + "version": "1.0", + "generics": [ + { + "name": "Animal", + "namespace": "Testing", + "attributes": [{"name": "name", "kind": "Text"}], + "restricted_namespaces": ["Dog"], + } + ], + "nodes": [ + { + "name": "Cat", + "namespace": "Cat", + "inherit_from": ["TestingAnimal"], + "attributes": [{"name": "breed", "kind": "Text", "optional": True}], + } + ], + } + + error_message = ( + "Generic node 'TestingAnimal' has restricted namespaces: ['Dog']. " + "The node 'CatCat' does not comply with this restriction as its namespace is 'Cat'." + ) + + httpx_mock.add_response( + method="POST", + url="http://mock/api/schema/load?branch=main", + status_code=422, + json={ + "data": None, + "errors": [{"message": error_message, "extensions": {"code": 422}}], + }, + ) + + # Act + response = await client.schema.load(schemas=[schema_payload]) + + # Assert + assert response.errors + error_message = response.errors["errors"][0]["message"] + assert re.search(r"(?s)restricted namespaces(?=.*Dog)(?=.*Cat)", error_message) + assert not response.schema_updated