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()