diff --git a/test/unit_tests/domain/conftest.py b/test/unit_tests/domain/conftest.py index fe60e05d5..6f5096491 100644 --- a/test/unit_tests/domain/conftest.py +++ b/test/unit_tests/domain/conftest.py @@ -1,3 +1,7 @@ +from test.unit_tests.domain.products.product_blocks.product_block_list_nested import ( # noqa: F401 # noqa: F401 + test_product_block_list_nested, + test_product_block_list_nested_db_parent, +) from test.unit_tests.domain.products.product_blocks.product_block_one import ( # noqa: F401 # noqa: F401 test_product_block_one, test_product_block_one_db, @@ -22,6 +26,11 @@ test_product_sub_block_two, test_product_sub_block_two_db, ) +from test.unit_tests.domain.products.product_types.product_type_list_nested import ( # noqa: F401 + test_product_list_nested, + test_product_model_list_nested, + test_product_type_list_nested, +) from test.unit_tests.domain.products.product_types.product_type_list_union import ( # noqa: F401 test_product_list_union, test_product_type_list_union, diff --git a/test/unit_tests/domain/products/product_blocks/product_block_list_nested.py b/test/unit_tests/domain/products/product_blocks/product_block_list_nested.py new file mode 100644 index 000000000..dede6af26 --- /dev/null +++ b/test/unit_tests/domain/products/product_blocks/product_block_list_nested.py @@ -0,0 +1,50 @@ +from typing import List, Optional + +import pytest + +from orchestrator.db import ProductBlockTable, db +from orchestrator.domain.base import ProductBlockModel +from orchestrator.types import SubscriptionLifecycle + + +class ProductBlockListNestedForTestInactive(ProductBlockModel, product_block_name="ProductBlockListNestedForTest"): + sub_block_list: List["ProductBlockListNestedForTestInactive"] + int_field: Optional[int] = None + + +class ProductBlockListNestedForTestProvisioning( + ProductBlockListNestedForTestInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING] +): + sub_block_list: List["ProductBlockListNestedForTestProvisioning"] # type: ignore + int_field: int + + +class ProductBlockListNestedForTest( + ProductBlockListNestedForTestProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE] +): + sub_block_list: List["ProductBlockListNestedForTest"] # type: ignore + int_field: int + + +@pytest.fixture +def test_product_block_list_nested(): + # Classes defined at module level, otherwise they remain in local namespace and + # `get_type_hints()` can't evaluate the ForwardRefs + return ( + ProductBlockListNestedForTestInactive, + ProductBlockListNestedForTestProvisioning, + ProductBlockListNestedForTest, + ) + + +@pytest.fixture +def test_product_block_list_nested_db_parent(resource_type_list, resource_type_int, resource_type_str): + parent_block = ProductBlockTable( + name="ProductBlockListNestedForTest", description="Test Block Parent", tag="TEST", status="active" + ) + parent_block.resource_types = [resource_type_int] + + db.session.add(parent_block) + db.session.commit() + + return parent_block diff --git a/test/unit_tests/domain/products/product_blocks/product_block_one_nested.py b/test/unit_tests/domain/products/product_blocks/product_block_one_nested.py index 80cf4f3e7..795aaa3b3 100644 --- a/test/unit_tests/domain/products/product_blocks/product_block_one_nested.py +++ b/test/unit_tests/domain/products/product_blocks/product_block_one_nested.py @@ -7,10 +7,8 @@ from orchestrator.types import SubscriptionLifecycle -# TODO also test List class ProductBlockOneNestedForTestInactive(ProductBlockModel, product_block_name="ProductBlockOneNestedForTest"): sub_block: Optional["ProductBlockOneNestedForTestInactive"] = None - # sub_block_list: List[ForwardRef("ProductBlockOneNestedForTestInactive")] = [] int_field: Optional[int] = None @@ -18,13 +16,11 @@ class ProductBlockOneNestedForTestProvisioning( ProductBlockOneNestedForTestInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING] ): sub_block: Optional["ProductBlockOneNestedForTestProvisioning"] = None - # sub_block_list: List[ForwardRef("ProductBlockOneNestedForTestProvisioning")] int_field: int class ProductBlockOneNestedForTest(ProductBlockOneNestedForTestProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): sub_block: Optional["ProductBlockOneNestedForTest"] = None - # sub_block_list: List[ForwardRef("ProductBlockOneNestedForTest")] int_field: int diff --git a/test/unit_tests/domain/products/product_types/product_type_list_nested.py b/test/unit_tests/domain/products/product_types/product_type_list_nested.py new file mode 100644 index 000000000..394a59403 --- /dev/null +++ b/test/unit_tests/domain/products/product_types/product_type_list_nested.py @@ -0,0 +1,65 @@ +import pytest + +from orchestrator.db import FixedInputTable, ProductTable, db +from orchestrator.domain import SUBSCRIPTION_MODEL_REGISTRY +from orchestrator.domain.base import ProductModel, SubscriptionModel +from orchestrator.domain.lifecycle import ProductLifecycle +from orchestrator.types import SubscriptionLifecycle +from test.unit_tests.domain.products.product_blocks.product_block_list_nested import ( + ProductBlockListNestedForTest, + ProductBlockListNestedForTestInactive, + ProductBlockListNestedForTestProvisioning, +) + + +@pytest.fixture +def test_product_type_list_nested(): + class ProductTypeListNestedForTestInactive(SubscriptionModel, is_base=True): + test_fixed_input: bool + block: ProductBlockListNestedForTestInactive + + class ProductTypeListNestedForTestProvisioning( + ProductTypeListNestedForTestInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING] + ): + test_fixed_input: bool + block: ProductBlockListNestedForTestProvisioning + + class ProductTypeListNestedForTest( + ProductTypeListNestedForTestProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE] + ): + test_fixed_input: bool + block: ProductBlockListNestedForTest + + SUBSCRIPTION_MODEL_REGISTRY["TestProductListNested"] = ProductTypeListNestedForTest + yield ProductTypeListNestedForTestInactive, ProductTypeListNestedForTestProvisioning, ProductTypeListNestedForTest + del SUBSCRIPTION_MODEL_REGISTRY["TestProductListNested"] + + +@pytest.fixture +def test_product_list_nested(test_product_block_list_nested_db_parent): + product = ProductTable( + name="TestProductListNested", description="Test ProductTable", product_type="Test", tag="TEST", status="active" + ) + + fixed_input = FixedInputTable(name="test_fixed_input", value="False") + + product_block = test_product_block_list_nested_db_parent + product.fixed_inputs = [fixed_input] + product.product_blocks = [product_block] + + db.session.add(product) + db.session.commit() + + return product.product_id + + +@pytest.fixture +def test_product_model_list_nested(test_product_list_nested): + return ProductModel( + product_id=test_product_list_nested, + name="TestProductListNested", + description="Test ProductTable", + product_type="Test", + tag="TEST", + status=ProductLifecycle.ACTIVE, + ) diff --git a/test/unit_tests/domain/test_base.py b/test/unit_tests/domain/test_base.py index 32a14034e..1216bf39c 100644 --- a/test/unit_tests/domain/test_base.py +++ b/test/unit_tests/domain/test_base.py @@ -26,10 +26,10 @@ ) from orchestrator.domain.lifecycle import ProductLifecycle from orchestrator.types import SubscriptionLifecycle -from test.unit_tests.domain.products.product_blocks.product_block_one_nested import ( - ProductBlockOneNestedForTest, - ProductBlockOneNestedForTestInactive, +from test.unit_tests.domain.products.product_blocks.product_block_list_nested import ( + ProductBlockListNestedForTestInactive, ) +from test.unit_tests.domain.products.product_blocks.product_block_one_nested import ProductBlockOneNestedForTestInactive def test_product_block_metadata(test_product_block_one, test_product_one, test_product_block_one_db): @@ -48,7 +48,7 @@ def test_product_block_metadata(test_product_block_one, test_product_one, test_p assert ProductBlockOneForTestInactive.tag == "TEST" -def test_product_block_nested(test_product_model_nested, test_product_type_one_nested): +def test_product_block_one_nested(test_product_model_nested, test_product_type_one_nested): """Test the behavior of nesting (self-referencing) product blocks. Notes: @@ -82,7 +82,7 @@ def test_product_block_nested(test_product_model_nested, test_product_type_one_n start_date=None, status=SubscriptionLifecycle.INITIAL, ) - model20.block = ProductBlockOneNestedForTest.new( + model20.block = ProductBlockOneNestedForTestInactive.new( subscription_id=model20.subscription_id, int_field=20, sub_block=model30.block, @@ -169,8 +169,164 @@ def test_product_block_nested(test_product_model_nested, test_product_type_one_n assert newmodel11.block.sub_block.sub_block.sub_block is None -# TODO add test for List of nested blocks -# def test_product_block_nested_list(test_product_model_nested, test_product_type_one_nested): +def test_product_block_list_nested(test_product_model_list_nested, test_product_type_list_nested): + """Test the behavior of nesting (self-referencing) a list of product blocks. + + Notes: + - nesting only works when each block is attached to a different subscription + """ + ProductTypeListNestedForTestInactive, _, ProductTypeListNestedForTest = test_product_type_list_nested + + customer_id = uuid4() + # Create productblocks 30 and 31 that will both be nested in block 20 and 21 + model30 = ProductTypeListNestedForTestInactive.from_product_id( + product_id=test_product_model_list_nested.product_id, + customer_id=customer_id, + insync=True, + start_date=None, + status=SubscriptionLifecycle.INITIAL, + ) + model30.block = ProductBlockListNestedForTestInactive.new( + subscription_id=model30.subscription_id, + int_field=30, + sub_block_list=[], + ) + model30 = SubscriptionModel.from_other_lifecycle(model30, SubscriptionLifecycle.ACTIVE) + model30.save() + model31 = ProductTypeListNestedForTestInactive.from_product_id( + product_id=test_product_model_list_nested.product_id, + customer_id=customer_id, + insync=True, + start_date=None, + status=SubscriptionLifecycle.INITIAL, + ) + model31.block = ProductBlockListNestedForTestInactive.new( + subscription_id=model31.subscription_id, + int_field=31, + sub_block_list=[], + ) + model31 = SubscriptionModel.from_other_lifecycle(model31, SubscriptionLifecycle.ACTIVE) + model31.save() + db.session.commit() + + # Create productblocks 20 and 21 that both + # - refer to blocks 30 and 31 + # - will be nested in blocks 10 and 11 + model20 = ProductTypeListNestedForTestInactive.from_product_id( + product_id=test_product_model_list_nested.product_id, + customer_id=customer_id, + insync=True, + start_date=None, + status=SubscriptionLifecycle.INITIAL, + ) + model20.block = ProductBlockListNestedForTestInactive.new( + subscription_id=model20.subscription_id, + int_field=20, + sub_block_list=[model30.block, model31.block], + ) + model20 = SubscriptionModel.from_other_lifecycle(model20, SubscriptionLifecycle.ACTIVE) + model20.save() + model21 = ProductTypeListNestedForTestInactive.from_product_id( + product_id=test_product_model_list_nested.product_id, + customer_id=customer_id, + insync=True, + start_date=None, + status=SubscriptionLifecycle.INITIAL, + ) + model21.block = ProductBlockListNestedForTestInactive.new( + subscription_id=model21.subscription_id, + int_field=21, + sub_block_list=[model30.block, model31.block], + ) + model21 = SubscriptionModel.from_other_lifecycle(model21, SubscriptionLifecycle.ACTIVE) + model21.save() + db.session.commit() + + # Create productblocks 10 and 11 that both refer to blocks 20 and 21 + model10 = ProductTypeListNestedForTestInactive.from_product_id( + product_id=test_product_model_list_nested.product_id, + customer_id=customer_id, + insync=True, + start_date=None, + status=SubscriptionLifecycle.INITIAL, + ) + model10.block = ProductBlockListNestedForTestInactive.new( + subscription_id=model10.subscription_id, + int_field=10, + sub_block_list=[model20.block, model21.block], + ) + model10 = SubscriptionModel.from_other_lifecycle(model10, SubscriptionLifecycle.ACTIVE) + model10.save() + model11 = ProductTypeListNestedForTestInactive.from_product_id( + product_id=test_product_model_list_nested.product_id, + customer_id=customer_id, + insync=True, + start_date=None, + status=SubscriptionLifecycle.INITIAL, + ) + model11.block = ProductBlockListNestedForTestInactive.new( + subscription_id=model11.subscription_id, + int_field=11, + sub_block_list=[model20.block, model21.block], + ) + model11 = SubscriptionModel.from_other_lifecycle(model11, SubscriptionLifecycle.ACTIVE) + model11.save() + db.session.commit() + + # Load blocks 10 and 11 and verify the nested blocks + newmodel10 = ProductTypeListNestedForTest.from_subscription(model10.subscription_id) + newmodel11 = ProductTypeListNestedForTest.from_subscription(model11.subscription_id) + assert newmodel10.block.int_field == 10 + assert newmodel11.block.int_field == 11 + assert newmodel10.block.sub_block_list[0].int_field == 20 + assert newmodel10.block.sub_block_list[1].int_field == 21 + assert newmodel11.block.sub_block_list[0].int_field == 20 + assert newmodel11.block.sub_block_list[1].int_field == 21 + assert sorted( + level3.int_field for level2 in newmodel10.block.sub_block_list for level3 in level2.sub_block_list + ) == [30, 30, 31, 31] + assert sorted( + level3.int_field for level2 in newmodel11.block.sub_block_list for level3 in level2.sub_block_list + ) == [30, 30, 31, 31] + # Assert a few blocks at deepest level to have an empty list + assert newmodel10.block.sub_block_list[0].sub_block_list[0].sub_block_list == [] + assert newmodel11.block.sub_block_list[1].sub_block_list[1].sub_block_list == [] + + # Load block 20 and verify nested blocks + newmodel20 = ProductTypeListNestedForTest.from_subscription(model20.subscription_id) + assert newmodel20.block.int_field == 20 + assert newmodel20.block.sub_block_list[0].int_field == 30 + assert newmodel20.block.sub_block_list[1].int_field == 31 + assert newmodel20.block.sub_block_list[0].sub_block_list == [] + + # Create productblock 32 and nest it in block 20 (but not 21!) + model32 = ProductTypeListNestedForTestInactive.from_product_id( + product_id=test_product_model_list_nested.product_id, + customer_id=customer_id, + insync=True, + start_date=None, + status=SubscriptionLifecycle.INITIAL, + ) + model32.block = ProductBlockListNestedForTestInactive.new( + subscription_id=model30.subscription_id, + int_field=32, + sub_block_list=[], + ) + model32 = SubscriptionModel.from_other_lifecycle(model32, SubscriptionLifecycle.ACTIVE) + model32.save() + newmodel20.block.sub_block_list.append(model32.block) + newmodel20.save() + db.session.commit() + + # (again) Load blocks 10 and 11 and verify block 32 is present once + newmodel10 = ProductTypeListNestedForTest.from_subscription(model10.subscription_id) + newmodel11 = ProductTypeListNestedForTest.from_subscription(model11.subscription_id) + assert sorted( + level3.int_field for level2 in newmodel10.block.sub_block_list for level3 in level2.sub_block_list + ) == [30, 30, 31, 31, 32] + assert sorted( + level3.int_field for level2 in newmodel11.block.sub_block_list for level3 in level2.sub_block_list + ) == [30, 30, 31, 31, 32] def test_lifecycle(test_product_model, test_product_type_one, test_product_block_one):