fix(namespace): Namespaces relationships should not create a loop

Description

Abstract

Ensure that when settings a parent namespace to a namespace, we don’t create a loop, a loop being having a namespace descendant be the parent of one of its ascendant.

Motivation

It’s not possible to have a namespace belonging to a namespace that is one of its children namespace, or even itself.

Rationale

This is done via a method decorated via field_validator, with a simple recursive loop.

Info

Hash

afeb5f86809b05cf3ef131a59de1ed9235d59a8d

Date

2020-09-26 22:38:26 +0200

Parents
  • fix(bdd): Rename “can/cannot be none” describing scenarios [cf1ea754]2020-09-26 17:13:51 +0200

Children
  • fix(entities): Entities `id` field are frozen once set [ef8edc20]2020-09-27 09:56:59 +0200

Branches
Tags

(No tags)

Changes

isshub/domain/contexts/code_repository/entities/namespace/__init__.py

Type

Modified

Stats

+33 -1

@@ -1,10 +1,11 @@
 """Package defining the ``Namespace`` entity."""

 import enum
-from typing import Optional
+from typing import Any, Optional

 from isshub.domain.utils.entity import (
     BaseModelWithId,
+    field_validator,
     optional_field,
     required_field,
     validated,
@@ -69,3 +70,34 @@ class Namespace(_Namespace):
     """

     namespace: Optional[_Namespace] = optional_field(_Namespace)  # type: ignore
+
+    @field_validator(namespace)  # type: ignore
+    def validate_namespace_is_not_in_a_loop(  # noqa  # pylint: disable=unused-argument
+        self, field: Any, value: Any
+    ) -> None:
+        """Validate that the ``namespace`` field is not in a loop.
+
+        Being in a loop means that one of the descendants is the parent of one of the ascendants.
+
+        Parameters
+        ----------
+        field : Any
+            The field to validate. Passed via the ``@field_validator`` decorator.
+        value : Any
+            The value to validate for the `field`.
+
+        Raises
+        ------
+        ValueError
+            If the given namespace (`value`) is in a loop
+
+        """
+        if not value:
+            return
+
+        parent = value
+        while parent := parent.namespace:
+            if parent == value:
+                raise ValueError(
+                    f"{self.__class__.__name__}.namespace cannot be in a loop"
+                )

isshub/domain/contexts/code_repository/entities/namespace/features/describe.feature

Type

Modified

Stats

+10 -0

@@ -59,3 +59,13 @@ Feature: Describing a Namespace
     Scenario: A Namespace kind is mandatory
         Given a Namespace
         Then its kind is mandatory
+
+    Scenario: A Namespace cannot be contained in itself
+        Given a Namespace
+        Then its namespace cannot be itself
+
+    Scenario: A Namespace namespace cannot be in a loop
+        Given a Namespace
+        And a second Namespace
+        And a third Namespace
+        Then we cannot create a relationships loop with these namespaces

isshub/domain/contexts/code_repository/entities/namespace/tests/test_describe.py

Type

Modified

Stats

+50 -0

@@ -86,3 +86,53 @@ def namespace_field_is_mandatory(namespace_factory, field_name):
 @then(parsers.parse("its {field_name:w} is optional"))
 def namespace_field_is_optional(namespace_factory, field_name):
     check_field_nullable(namespace_factory, field_name)
+
+
+@scenario("../features/describe.feature", "A Namespace cannot be contained in itself")
+def test_namespace_namespace_cannot_be_itself():
+    pass
+
+
+@then("its namespace cannot be itself")
+def namespace_namespace_cannot_be_itself(namespace):
+    namespace.namespace = namespace
+    with pytest.raises(ValueError):
+        namespace.validate()
+
+
+@scenario("../features/describe.feature", "A Namespace namespace cannot be in a loop")
+def test_namespace_namespace_cannot_be_in_a_loop():
+    pass
+
+
+@given("a second Namespace", target_fixture="namespace2")
+def a_second_namespace(namespace_factory):
+    return namespace_factory()
+
+
+@given("a third Namespace", target_fixture="namespace3")
+def a_third_namespace(namespace_factory):
+    return namespace_factory()
+
+
+@then("we cannot create a relationships loop with these namespaces")
+def namespace_relationships_cannot_create_a_loop(namespace, namespace2, namespace3):
+    namespace2.namespace = namespace3
+    namespace3.validate()
+    namespace2.validate()
+    namespace.validate()
+    namespace.namespace = namespace2
+    namespace3.validate()
+    namespace2.validate()
+    namespace.validate()
+    namespace3.namespace = namespace
+    with pytest.raises(ValueError):
+        namespace3.validate()
+    with pytest.raises(ValueError):
+        namespace2.validate()
+    with pytest.raises(ValueError):
+        namespace.validate()
+    namespace3.namespace = None
+    namespace3.validate()
+    namespace2.validate()
+    namespace.validate()