diff --git a/test/e2e/mock_schemas.py b/test/e2e/mock_schemas.py index b33eebb5..13fb176f 100644 --- a/test/e2e/mock_schemas.py +++ b/test/e2e/mock_schemas.py @@ -4,67 +4,13 @@ # Leaf with allowed values in properties -from unittest.mock import ANY - -CREATED_MODIFIED_VALUES_EXPECTED = {"created_time": ANY, "modified_time": ANY} -# pylint: disable=duplicate-code -CATALOGUE_CATEGORY_POST_ALLOWED_VALUES = { - "name": "Category Allowed Values", - "is_leaf": True, - "properties": [ - { - "name": "Property A", - "type": "number", - "unit": "mm", - "mandatory": False, - "allowed_values": {"type": "list", "values": [2, 4, 6]}, - }, - { - "name": "Property B", - "type": "string", - "unit": None, - "mandatory": True, - "allowed_values": {"type": "list", "values": ["red", "green"]}, - }, - ], -} - -# To be posted on CATALOGUE_CATEGORY_POST_ALLOWED_VALUES -CATALOGUE_ITEM_POST_ALLOWED_VALUES = { - "name": "Catalogue Item D", - "description": "This is Catalogue Item D", - "cost_gbp": 300.00, - "cost_to_rework_gbp": 120.99, - "days_to_replace": 1.5, - "days_to_rework": 3.0, - "drawing_number": "789xyz", - "is_obsolete": False, - "properties": [{"name": "Property A", "value": 4}, {"name": "Property B", "value": "red"}], -} +# pylint: disable=fixme +# TODO: Remove this file - replace by mock_data.py -ITEM_POST_ALLOWED_VALUES = { - "is_defective": False, - "warranty_end_date": "2015-11-15T23:59:59Z", - "serial_number": "xyz123", - "delivered_date": "2012-12-05T12:00:00Z", - "notes": "Test notes", - "properties": [{"name": "Property A", "value": 6}, {"name": "Property B", "value": "green"}], -} +from unittest.mock import ANY -ITEM_POST_ALLOWED_VALUES_EXPECTED = { - **ITEM_POST_ALLOWED_VALUES, - **CREATED_MODIFIED_VALUES_EXPECTED, - "id": ANY, - "purchase_order_number": None, - "usage_status": "New", - "usage_status_id": ANY, - "asset_number": None, - "properties": [ - {"name": "Property A", "unit": "mm", "value": 6}, - {"name": "Property B", "value": "green", "unit": None}, - ], -} +CREATED_MODIFIED_VALUES_EXPECTED = {"created_time": ANY, "modified_time": ANY} SYSTEM_POST_A = { "name": "System A", diff --git a/test/e2e/test_catalogue_category.py b/test/e2e/test_catalogue_category.py index 77654330..e7fd4c15 100644 --- a/test/e2e/test_catalogue_category.py +++ b/test/e2e/test_catalogue_category.py @@ -1,9 +1,9 @@ -# pylint: disable=too-many-lines """ End-to-End tests for the catalogue category router. """ # Expect some duplicate code inside tests as the tests for the different entities can be very similar +# pylint: disable=too-many-lines # pylint: disable=duplicate-code # pylint: disable=too-many-public-methods @@ -53,15 +53,6 @@ def setup(self, test_client): self.test_client = test_client self.unit_value_id_dict = {} - def post_unit(self, unit_post_data: dict) -> None: - """Posts a unit with the given data and stores the value and ID in a dictionary for lookup later. - - :param unit_post_data: Dictionary containing the unit data as would be required for a `UnitPostSchema`. - """ - - post_response = self.test_client.post("/v1/units", json=unit_post_data) - self.unit_value_id_dict[unit_post_data["value"]] = post_response.json()["id"] - def add_unit_value_and_id(self, unit_value: str, unit_id: str) -> None: """ Stores a unit value and ID inside the `unit_value_id_dict` for tests that need to have a @@ -73,6 +64,15 @@ def add_unit_value_and_id(self, unit_value: str, unit_id: str) -> None: self.unit_value_id_dict[unit_value] = unit_id + def post_unit(self, unit_post_data: dict) -> None: + """Posts a unit with the given data and stores the value and ID in a dictionary for lookup later. + + :param unit_post_data: Dictionary containing the unit data as would be required for a `UnitPostSchema`. + """ + + post_response = self.test_client.post("/v1/units", json=unit_post_data) + self.add_unit_value_and_id(unit_post_data["value"], post_response.json()["id"]) + def post_catalogue_category(self, catalogue_category_data: dict) -> Optional[str]: """ Posts a catalogue category with the given data and returns the ID of the created catalogue category if @@ -406,9 +406,10 @@ def check_get_catalogue_category_success(self, expected_catalogue_category_get_d """ Checks that a prior call to `get_catalogue_category` gave a successful response with the expected data returned. - :param expected_catalogue_category_get_data: Dictionary containing the expected system data returned as would - be required for a `CatalogueCategorySchema`. Does not need unit IDs - as they will be added automatically to check they are as expected. + :param expected_catalogue_category_get_data: Dictionary containing the expected catalogue category data returned + as would be required for a `CatalogueCategorySchema`. Does not need + unit IDs as they will be added automatically to check they are as + expected. """ assert self._get_response_catalogue_category.status_code == 200 @@ -596,7 +597,7 @@ class ListDSL(GetBreadcrumbsDSL): def get_catalogue_categories(self, filters: dict) -> None: """ - Gets a list catalogue categories with the given filters. + Gets a list of catalogue categories with the given filters. :param filters: Filters to use in the request. """ @@ -627,8 +628,8 @@ def check_get_catalogue_categories_success(self, expected_catalogue_categories_g Checks that a prior call to `get_catalogue_categories` gave a successful response with the expected data returned. - :param expected_catalogue_categories_get_data: List of dictionaries containing the expected system data - returned as would be required for `CatalogueCategorySchema`'s. + :param expected_catalogue_categories_get_data: List of dictionaries containing the expected catalogue category + data returned as would be required for `CatalogueCategorySchema`'s. """ assert self._get_response_catalogue_category.status_code == 200 @@ -777,9 +778,9 @@ def check_patch_catalogue_category_response_success(self, expected_catalogue_cat Checks that a prior call to `patch_catalogue_category` gave a successful response with the expected data returned. - :param expected_catalogue_category_get_data: Dictionary containing the expected system data returned as would - be required for a `CatalogueCategorySchema`. Does not need unit IDs - as they will be added automatically to check they are as expected. + :param expected_catalogue_category_get_data: Dictionary containing the expected catalogue category data returned + as would be required for a `CatalogueCategorySchema`. Does not need unit IDs + as they will be added automatically to check they are as expected. """ assert self._patch_response_catalogue_category.status_code == 200 diff --git a/test/e2e/test_catalogue_item.py b/test/e2e/test_catalogue_item.py index 1b39b0dc..cb23fe63 100644 --- a/test/e2e/test_catalogue_item.py +++ b/test/e2e/test_catalogue_item.py @@ -1,9 +1,9 @@ -# pylint: disable=too-many-lines """ End-to-End tests for the catalogue item router. """ # Expect some duplicate code inside tests as the tests for the different entities can be very similar +# pylint: disable=too-many-lines # pylint: disable=duplicate-code # pylint: disable=too-many-public-methods @@ -82,7 +82,7 @@ def add_ids_to_expected_catalogue_item_get_data(self, expected_catalogue_item_ge :param expected_catalogue_item_get_data: Dictionary containing the expected catalogue item data returned as would be required for a `CatalogueItemSchema`. Does not need mandatory - IDs (e.g. manufacturer_id) as they will be added here. + IDs (e.g. `manufacturer_id`) as they will be added here. """ # Where there are properties add the property ID, unit ID and unit value expected_catalogue_item_get_data = E2ETestHelpers.add_property_ids_to_properties( @@ -134,8 +134,7 @@ def post_manufacturer(self, manufacturer_post_data: dict) -> Optional[str]: def post_catalogue_item(self, catalogue_item_data: dict) -> Optional[str]: """ - Posts a catalogue item with the given data and returns the ID of the created catalogue item if - successful. + Posts a catalogue item with the given data and returns the ID of the created catalogue item if successful. :param catalogue_item_data: Dictionary containing the basic catalogue item data as would be required for a `CatalogueItemPostSchema` but with mandatory IDs missing and @@ -261,9 +260,7 @@ def post_catalogue_item_and_prerequisites_with_allowed_values( return self.post_catalogue_item( { **CATALOGUE_ITEM_DATA_WITH_MANDATORY_PROPERTIES_ONLY, - "properties": [ - {"name": "property", "value": property_value}, - ], + "properties": [{"name": "property", "value": property_value}], } ) @@ -273,9 +270,8 @@ def check_post_catalogue_item_success(self, expected_catalogue_item_get_data: di returned. :param expected_catalogue_item_get_data: Dictionary containing the expected catalogue item data returned as - would be required for a `CatalogueItemSchema`. Does not need mandatory - IDs (e.g. manufacturer_id) as they will be to check they are as - expected. + would be required for a `CatalogueItemSchema`. Does not need mandatory IDs (e.g. + `manufacturer_id`) as they will be added automatically to check they are as expected. """ assert self._post_response_catalogue_item.status_code == 201 @@ -376,7 +372,7 @@ def test_create_obsolete_with_invalid_obsolete_replacement_catalogue_item_id(sel 422, "The specified replacement catalogue item does not exist" ) - def test_create_with_all_properties_defined(self): + def test_create_with_all_properties_provided(self): """Test creating a catalogue item with all properties within the catalogue category being defined.""" self.post_catalogue_item_and_prerequisites_with_properties(CATALOGUE_ITEM_DATA_WITH_ALL_PROPERTIES) @@ -475,7 +471,7 @@ def test_create_with_boolean_property_with_invalid_value_type(self): self.post_catalogue_item_and_prerequisites_with_given_properties( catalogue_category_properties_data=[CATALOGUE_CATEGORY_PROPERTY_DATA_BOOLEAN_MANDATORY], catalogue_item_properties_data=[ - {"name": CATALOGUE_CATEGORY_PROPERTY_DATA_BOOLEAN_MANDATORY["name"], "value": "True"}, + {"name": CATALOGUE_CATEGORY_PROPERTY_DATA_BOOLEAN_MANDATORY["name"], "value": 0}, ], ) @@ -486,8 +482,32 @@ def test_create_with_boolean_property_with_invalid_value_type(self): "'. Expected type: boolean.", ) - def test_create_with_invalid_string_allowed_values_list_value(self): - """Test creating a catalogue item with an invalid value for a string property with an allowed values list.""" + def test_create_with_allowed_values_list(self): + """Test creating a catalogue item with properties that have allowed values lists.""" + + self.post_catalogue_item_and_prerequisites_with_given_properties( + catalogue_category_properties_data=[ + CATALOGUE_CATEGORY_PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST, + CATALOGUE_CATEGORY_PROPERTY_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST, + ], + catalogue_item_properties_data=[ + PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_1, + PROPERTY_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE1, + ], + ) + self.check_post_catalogue_item_success( + { + **CATALOGUE_ITEM_GET_DATA_WITH_ALL_PROPERTIES, + "properties": [ + PROPERTY_GET_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_1, + PROPERTY_GET_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE1, + ], + } + ) + + def test_create_with_string_property_with_allowed_values_list_with_invalid_value(self): + """Test creating a catalogue item with a string property with an allowed values list while giving it a value not + in the list.""" self.post_catalogue_item_and_prerequisites_with_allowed_values( "string", {"type": "list", "values": ["value1"]}, "value2" @@ -498,8 +518,9 @@ def test_create_with_invalid_string_allowed_values_list_value(self): "Expected one of value1.", ) - def test_create_with_invalid_string_allowed_values_list_type(self): - """Test creating a catalogue item with an invalid type for a string property with an allowed values list.""" + def test_create_with_string_property_with_allowed_values_list_with_invalid_type(self): + """Test creating a catalogue item with a string property with an allowed values list while giving it a value + with an incorrect type.""" self.post_catalogue_item_and_prerequisites_with_allowed_values( "string", {"type": "list", "values": ["value1"]}, 42 @@ -510,8 +531,9 @@ def test_create_with_invalid_string_allowed_values_list_type(self): "Expected type: string.", ) - def test_create_with_invalid_number_allowed_values_list_value(self): - """Test creating a catalogue item with an invalid value for a number property with an allowed values list.""" + def test_create_with_number_property_with_allowed_values_list_with_invalid_value(self): + """Test creating a catalogue item with a number property with an allowed values list while giving it a value not + in the list.""" self.post_catalogue_item_and_prerequisites_with_allowed_values("number", {"type": "list", "values": [1]}, 2) self.check_post_catalogue_item_failed_with_detail( @@ -519,8 +541,9 @@ def test_create_with_invalid_number_allowed_values_list_value(self): f"Invalid value for property with ID '{self.property_name_id_dict['property']}'. Expected one of 1.", ) - def test_create_with_invalid_number_allowed_values_list_type(self): - """Test creating a catalogue item with an invalid type for a number property with an allowed values list.""" + def test_create_with_number_property_with_allowed_values_list_with_invalid_type(self): + """Test creating a catalogue item with a number property with an allowed values list while giving it a value + with an incorrect type.""" self.post_catalogue_item_and_prerequisites_with_allowed_values( "number", {"type": "list", "values": [1]}, "test" @@ -597,10 +620,10 @@ def check_get_catalogue_item_success(self, expected_catalogue_item_get_data: dic """ Checks that a prior call to `get_catalogue_item` gave a successful response with the expected data returned. - :param expected_catalogue_item_get_data: Dictionary containing the expected system data returned as would be - required for a `CatalogueItemSchema`. Does not need mandatory IDs (e.g. - manufacturer_id) as they will be added automatically to check they are - as expected. + :param expected_catalogue_item_get_data: Dictionary containing the expected catalogue tiem data returned as + would be required for a `CatalogueItemSchema`. Does not need mandatory + IDs (e.g. `manufacturer_id`) as they will be added automatically to + check they are as expected. """ assert self._get_response_catalogue_item.status_code == 200 @@ -627,9 +650,9 @@ class TestGet(GetDSL): def test_get(self): """Test getting a catalogue item.""" - self.post_catalogue_category(CATALOGUE_CATEGORY_POST_DATA_LEAF_NO_PARENT_NO_PROPERTIES) - self.post_manufacturer(MANUFACTURER_POST_DATA_REQUIRED_VALUES_ONLY) - catalogue_item_id = self.post_catalogue_item(CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY) + catalogue_item_id = self.post_catalogue_item_and_prerequisites_no_properties( + CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY + ) self.get_catalogue_item(catalogue_item_id) self.check_get_catalogue_item_success(CATALOGUE_ITEM_GET_DATA_REQUIRED_VALUES_ONLY) @@ -652,7 +675,7 @@ class ListDSL(GetDSL): def get_catalogue_items(self, filters: dict) -> None: """ - Gets a list catalogue items with the given filters. + Gets a list of catalogue items with the given filters. :param filters: Filters to use in the request. """ @@ -662,9 +685,9 @@ def get_catalogue_items(self, filters: dict) -> None: def post_test_catalogue_items(self) -> list[dict]: """ Posts two catalogue items each in a separate catalogue category and returns their expected responses when - returned by the ist endpoint. + returned by the list endpoint. - :return: List of dictionaries containing the expected catalogue category data returned from a get endpoint in + :return: List of dictionaries containing the expected catalogue item data returned from a get endpoint in the form of a `CatalogueItemSchema`. """ @@ -695,7 +718,7 @@ def check_get_catalogue_items_success(self, expected_catalogue_items_get_data: l """ Checks that a prior call to `get_catalogue_items` gave a successful response with the expected data returned. - :param expected_catalogue_items_get_data: List of dictionaries containing the expected system data + :param expected_catalogue_items_get_data: List of dictionaries containing the expected catalogue item data returned as would be required for `CatalogueItemSchema`'s. """ @@ -752,13 +775,13 @@ def patch_catalogue_item(self, catalogue_item_id: str, catalogue_item_update_dat """ Updates a catalogue item with the given ID. - :param catalogue_item_id: ID of the catalogue category to patch. + :param catalogue_item_id: ID of the catalogue item to patch. :param catalogue_item_update_data: Dictionary containing the basic patch data as would be required for a `CatalogueItemPatchSchema` but with any `id`'s replaced by the `name` value in its properties as the IDs will be added automatically. """ - # Replace any unit values with unit ids + # Replace any property names with ids catalogue_item_update_data = E2ETestHelpers.replace_property_names_with_ids_in_properties( catalogue_item_update_data, self.property_name_id_dict ) @@ -797,10 +820,10 @@ def check_patch_catalogue_item_response_success(self, expected_catalogue_item_ge Checks that a prior call to `patch_catalogue_item` gave a successful response with the expected data returned. - :param expected_catalogue_item_get_data: Dictionary containing the expected system data returned as would - be required for a `CatalogueItemSchema`. Does not need mandatory IDs - (e.g. manufacturer_id) as they will be added automatically to check - they are as expected. + :param expected_catalogue_item_get_data: Dictionary containing the expected catalogue item data returned as + would be required for a `CatalogueItemSchema`. Does not need mandatory IDs + (e.g. `manufacturer_id`) as they will be added automatically to check they are + as expected. """ assert self._patch_response_catalogue_item.status_code == 200 @@ -1341,7 +1364,7 @@ def test_partial_update_properties_with_boolean_property_with_invalid_value_type ) def test_partial_update_properties_with_allowed_values_list(self): - """Test updating the `properties` of a catalogue item that have a list of allowed values.""" + """Test updating the `properties` of a catalogue item that has allowed values lists.""" catalogue_item_id = self.post_catalogue_item_and_prerequisites_with_given_properties( catalogue_category_properties_data=[ @@ -1370,7 +1393,7 @@ def test_partial_update_properties_with_allowed_values_list(self): } ) - def test_partial_update_properties_with_string_allowed_values_list_to_invalid_value(self): + def test_partial_update_string_property_with_allowed_values_list_to_invalid_value(self): """Test updating the value of a string property with an allowed values list to be a value not in the list.""" catalogue_item_id = self.post_catalogue_item_and_prerequisites_with_allowed_values( @@ -1384,7 +1407,21 @@ def test_partial_update_properties_with_string_allowed_values_list_to_invalid_va "value2, value3.", ) - def test_partial_update_properties_with_number_allowed_values_list_to_invalid_value(self): + def test_partial_update_string_property_with_allowed_values_list_to_invalid_value_type(self): + """Test updating the value of a string property with an allowed values list to be the wrong type.""" + + catalogue_item_id = self.post_catalogue_item_and_prerequisites_with_allowed_values( + "string", {"type": "list", "values": ["value1"]}, "value1" + ) + + self.patch_catalogue_item(catalogue_item_id, {"properties": [{"name": "property", "value": 42}]}) + self.check_patch_catalogue_item_failed_with_detail( + 422, + f"Invalid value type for property with ID '{self.property_name_id_dict['property']}'. " + "Expected type: string.", + ) + + def test_partial_update_number_property_with_allowed_values_list_to_invalid_value(self): """Test updating the value of a number property with an allowed values list to be a value not in the list.""" catalogue_item_id = self.post_catalogue_item_and_prerequisites_with_allowed_values( @@ -1397,6 +1434,20 @@ def test_partial_update_properties_with_number_allowed_values_list_to_invalid_va f"Invalid value for property with ID '{self.property_name_id_dict['property']}'. Expected one of 1, 2, 3.", ) + def test_partial_update_number_property_with_allowed_values_list_to_invalid_value_type(self): + """Test updating the value of a number property with an allowed values list to be the wrong type.""" + + catalogue_item_id = self.post_catalogue_item_and_prerequisites_with_allowed_values( + "number", {"type": "list", "values": [1]}, 1 + ) + + self.patch_catalogue_item(catalogue_item_id, {"properties": [{"name": "property", "value": "2"}]}) + self.check_patch_catalogue_item_failed_with_detail( + 422, + f"Invalid value type for property with ID '{self.property_name_id_dict['property']}'. " + "Expected type: number.", + ) + def test_partial_update_properties_with_children(self): """Test updating the `properties` of a catalogue item when it has children.""" @@ -1505,9 +1556,9 @@ class TestDelete(DeleteDSL): def test_delete(self): """Test deleting a catalogue item.""" - self.post_catalogue_category(CATALOGUE_CATEGORY_POST_DATA_LEAF_NO_PARENT_NO_PROPERTIES) - self.post_manufacturer(MANUFACTURER_POST_DATA_REQUIRED_VALUES_ONLY) - catalogue_item_id = self.post_catalogue_item(CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY) + catalogue_item_id = self.post_catalogue_item_and_prerequisites_no_properties( + CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY + ) self.delete_catalogue_item(catalogue_item_id) self.check_delete_catalogue_item_success() @@ -1518,9 +1569,9 @@ def test_delete(self): def test_delete_with_child_item(self): """Test deleting a catalogue item with a child item.""" - self.post_catalogue_category(CATALOGUE_CATEGORY_POST_DATA_LEAF_NO_PARENT_NO_PROPERTIES) - self.post_manufacturer(MANUFACTURER_POST_DATA_REQUIRED_VALUES_ONLY) - catalogue_item_id = self.post_catalogue_item(CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY) + catalogue_item_id = self.post_catalogue_item_and_prerequisites_no_properties( + CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY + ) self.post_child_item() self.delete_catalogue_item(catalogue_item_id) diff --git a/test/e2e/test_item.py b/test/e2e/test_item.py index 4e67896a..0c0a1760 100644 --- a/test/e2e/test_item.py +++ b/test/e2e/test_item.py @@ -1,2285 +1,1209 @@ -# pylint: disable=too-many-lines """ -End-to-End tests for the catalogue item router. +End-to-End tests for the item router. """ -from test.conftest import add_ids_to_properties -from test.e2e.conftest import replace_unit_values_with_ids_in_properties -from test.e2e.mock_schemas import ( - CATALOGUE_CATEGORY_POST_ALLOWED_VALUES, - CATALOGUE_ITEM_POST_ALLOWED_VALUES, - CREATED_MODIFIED_VALUES_EXPECTED, - ITEM_POST_ALLOWED_VALUES, - ITEM_POST_ALLOWED_VALUES_EXPECTED, - SYSTEM_POST_A, - SYSTEM_POST_B, - USAGE_STATUS_POST_A, -) -from test.e2e.test_unit import UNIT_POST_A, UNIT_POST_B -from unittest.mock import ANY +# Expect some duplicate code inside tests as the tests for the different entities can be very similar +# pylint: disable=too-many-lines +# pylint: disable=duplicate-code +# pylint: disable=too-many-public-methods +# pylint: disable=too-many-ancestors + +from test.e2e.conftest import E2ETestHelpers +from test.e2e.test_catalogue_item import CreateDSL as CatalogueItemCreateDSL +from test.e2e.test_system import CreateDSL as SystemCreateDSL +from test.mock_data import ( + CATALOGUE_CATEGORY_PROPERTY_DATA_BOOLEAN_MANDATORY, + CATALOGUE_CATEGORY_PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST, + CATALOGUE_CATEGORY_PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT, + CATALOGUE_CATEGORY_PROPERTY_DATA_STRING_MANDATORY, + CATALOGUE_CATEGORY_PROPERTY_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST, + CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY, + CATALOGUE_ITEM_DATA_WITH_ALL_PROPERTIES, + ITEM_DATA_ALL_VALUES_NO_PROPERTIES, ITEM_DATA_REQUIRED_VALUES_ONLY, + ITEM_DATA_WITH_ALL_PROPERTIES, ITEM_GET_DATA_ALL_VALUES_NO_PROPERTIES, + ITEM_GET_DATA_REQUIRED_VALUES_ONLY, ITEM_GET_DATA_WITH_ALL_PROPERTIES, + PROPERTY_DATA_BOOLEAN_MANDATORY_FALSE, + PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_1, + PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_2, + PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT_1, + PROPERTY_DATA_STRING_MANDATORY_TEXT, + PROPERTY_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE1, + PROPERTY_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE2, + PROPERTY_GET_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_2, + PROPERTY_GET_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT_1, + PROPERTY_GET_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE2, + SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT, + SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY, USAGE_STATUS_DATA_IN_USE, + USAGE_STATUS_DATA_NEW) +from typing import Any, Optional + +import pytest from bson import ObjectId +from httpx import Response + + +class CreateDSL(CatalogueItemCreateDSL, SystemCreateDSL): + """Base class for create tests.""" + + catalogue_item_id: Optional[str] + system_id: Optional[str] + usage_status_value_id_dict: dict[str, str] + + _post_response_item: Response + + @pytest.fixture(autouse=True) + def setup_item_create_dsl(self): + """Setup fixtures""" + + self.catalogue_item_id = None + self.system_id = None + self.usage_status_value_id_dict = {} + + def merge_properties_in_expected_item_get_data(self, expected_item_get_data: dict) -> dict: + """ + Merges any existing properties in an already posted catalogue item into expected get data returned for an item. + + Assumes the last catalogue item posted was the one the properties should be merged with. + + :param expected_item_get_data: Dictionary containing the expected item data returned as would be required for a + `ItemSchema`. Does not need mandatory IDs (e.g. `system_id`). + :return: Dictionary containing the same `expected_item_get_data` but with any missing properties found inside + the last posted catalogue item merged in. + """ + + if "properties" in expected_item_get_data: + catalogue_item_data = self._post_response_catalogue_item.json() + expected_merged_properties = [] + supplied_properties_dict = { + supplied_property["name"]: supplied_property + for supplied_property in expected_item_get_data["properties"] + } + for prop in catalogue_item_data["properties"]: + supplied_property = supplied_properties_dict.get(prop["name"]) + expected_merged_properties.append(supplied_property if supplied_property else prop) + + return {**expected_item_get_data, "properties": expected_merged_properties} + return expected_item_get_data + + def add_ids_to_expected_item_get_data(self, expected_item_get_data) -> dict: + """ + Adds required IDs to some expected item get data based on what has already been posted. + + :param expected_item_get_data: Dictionary containing the expected item data returned as would be required for an + `ItemSchema`. Does not need mandatory IDs (e.g. `system_id`) as they will be + added here. + """ + # Where there are properties add the property ID, unit ID and unit value + expected_item_get_data = E2ETestHelpers.add_property_ids_to_properties( + expected_item_get_data, self.property_name_id_dict + ) + properties = [] + for prop in expected_item_get_data["properties"]: + properties.append({**prop, **self._unit_data_lookup_dict[prop["id"]]}) + expected_item_get_data = {**expected_item_get_data, "properties": properties} + + return { + **expected_item_get_data, + "catalogue_item_id": self.catalogue_item_id, + "system_id": self.system_id, + "usage_status_id": self.usage_status_value_id_dict[expected_item_get_data["usage_status"]], + } -# pylint: disable=duplicate-code -CATALOGUE_CATEGORY_POST_A = { - "name": "Category A", - "is_leaf": True, - "properties": [ - {"name": "Property A", "type": "number", "unit": "mm", "mandatory": False}, - {"name": "Property B", "type": "boolean", "mandatory": True}, - {"name": "Property C", "type": "string", "unit": "cm", "mandatory": True}, - {"name": "Property D", "type": "string", "mandatory": False}, - ], -} - -CATALOGUE_ITEM_POST_A = { - "name": "Catalogue Item A", - "description": "This is Catalogue Item A", - "cost_gbp": 129.99, - "days_to_replace": 2.0, - "drawing_link": "https://drawing-link.com/", - "item_model_number": "abc123", - "is_obsolete": False, - "properties": [ - {"name": "Property A", "value": 20}, - {"name": "Property B", "value": False}, - {"name": "Property C", "value": "20x15x10"}, - ], -} - -MANUFACTURER_POST = { - "name": "Manufacturer A", - "url": "http://example.com/", - "address": { - "address_line": "1 Example Street", - "town": "Oxford", - "county": "Oxfordshire", - "country": "United Kingdom", - "postcode": "OX1 2AB", - }, - "telephone": "0932348348", -} -# pylint: enable=duplicate-code - -ITEM_POST = { - "is_defective": False, - "warranty_end_date": "2015-11-15T23:59:59Z", - "serial_number": "xyz123", - "delivered_date": "2012-12-05T12:00:00Z", - "notes": "Test notes", - "properties": [{"name": "Property A", "value": 21}], -} - -ITEM_POST_EXPECTED = { - **ITEM_POST, - **CREATED_MODIFIED_VALUES_EXPECTED, - "id": ANY, - "purchase_order_number": None, - "asset_number": None, - "properties": [ - {"name": "Property A", "value": 21, "unit": "mm"}, - {"name": "Property B", "value": False, "unit": None}, - {"name": "Property C", "value": "20x15x10", "unit": "cm"}, - {"name": "Property D", "value": None, "unit": None}, - ], -} + def post_catalogue_item(self, catalogue_item_data: dict) -> Optional[str]: + """ + Posts a catalogue item with the given data and returns the ID of the created catalogue item if successful. + + :param catalogue_item_data: Dictionary containing the basic catalogue item data as would be required + for a `CatalogueItemPostSchema` but with mandatory IDs missing and + any `id`'s replaced by the `name` value in its properties as the + IDs will be added automatically. + :return: ID of the created catalogue item (or `None` if not successful). + """ + + self.catalogue_item_id = CatalogueItemCreateDSL.post_catalogue_item(self, catalogue_item_data) + return self.catalogue_item_id + + def post_system(self, system_post_data: dict) -> Optional[str]: + """ + Posts a system with the given data and returns the id of the created system if successful. + + :param system_post_data: Dictionary containing the system data as would be required for a + `SystemPostSchema`. + :return: ID of the created system (or `None` if not successful). + """ + + self.system_id = SystemCreateDSL.post_system(self, system_post_data) + return self.system_id + + def add_usage_status_value_and_id(self, usage_status_value: str, usage_status_id: str) -> None: + """ + Stores a usage status value and ID inside the `usage_status_value_id_dict` for tests that need to have a + non-existent or invalid usage status ID. + + :param usage_status_value: Value of the usage status. + :param usage_status_id: ID of the usage status. + """ + + self.usage_status_value_id_dict[usage_status_value] = usage_status_id + + def post_usage_status(self, usage_status_post_data: dict) -> str: + """Posts a usage status with the given data and stores the value and ID in a dictionary for lookup later. + + :param usage_status_post_data: Dictionary containing the usage status data as would be required for a + `UsageStatusPostSchema`. + """ + + post_response = self.test_client.post("/v1/usage-statuses", json=usage_status_post_data) + usage_status_id = post_response.json()["id"] + self.add_usage_status_value_and_id(usage_status_post_data["value"], post_response.json()["id"]) + return usage_status_id + + def post_item(self, item_data: dict) -> Optional[str]: + """ + Posts an item with the given data and returns the ID of the created item if successful. + + :param item_data: Dictionary containing the basic item data as would be required for a `ItemPostSchema` but with + mandatory IDs missing and any `id`'s replaced by the `name` value in its properties as the IDs + will be added automatically. + :return: ID of the created item (or `None` if not successful). + """ + + # Replace any unit values with unit IDs + full_item_data = item_data.copy() + full_item_data = E2ETestHelpers.replace_unit_values_with_ids_in_properties( + full_item_data, self.unit_value_id_dict + ) + full_item_data = E2ETestHelpers.replace_property_names_with_ids_in_properties( + full_item_data, self.property_name_id_dict + ) + # Insert mandatory IDs if they have been created + if self.catalogue_item_id: + full_item_data["catalogue_item_id"] = self.catalogue_item_id + if self.system_id: + full_item_data["system_id"] = self.system_id + full_item_data["usage_status_id"] = self.usage_status_value_id_dict.get(full_item_data["usage_status"]) -# pylint: disable=duplicate-code -def _post_units(test_client): - """Utility function for posting all mock units defined at the top of this file""" + self._post_response_item = self.test_client.post("/v1/items", json=full_item_data) - response = test_client.get("/v1/units") - units = response.json() + return self._post_response_item.json()["id"] if self._post_response_item.status_code == 201 else None - if response.json() == []: + def post_item_and_prerequisites_no_properties(self, item_data: dict) -> Optional[str]: + """ + Utility method that posts an item with the given data and also its prerequisite system, usage status, catalogue + item and catalogue category. Uses CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY for the catalogue item and + USAGE_STATUS_DATA_IN_USE for the usage status. - response = test_client.post("/v1/units", json=UNIT_POST_A) - unit_mm = response.json() + :param item_data: Dictionary containing the basic item data as would be required for a `ItemPostSchema` but with + mandatory IDs missing and any `id`'s replaced by the `name` value in its properties as the IDs + will be added automatically. + :return: ID of the created item (or `None` if not successful). + """ - response = test_client.post("/v1/units", json=UNIT_POST_B) - unit_cm = response.json() + self.catalogue_item_id = self.post_catalogue_item_and_prerequisites_no_properties( + CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY + ) + self.post_system(SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY) + self.post_usage_status(USAGE_STATUS_DATA_IN_USE) - units = [unit_mm, unit_cm] + return self.post_item(item_data) - return units + def post_item_and_prerequisites_with_properties(self, item_data: dict) -> Optional[str]: + """ + Utility method that posts an item with the given data and also its prerequisite system, usage status, catalogue + item and catalogue category. Uses CATALOGUE_ITEM_DATA_WITH_ALL_PROPERTIES for the catalogue item and + USAGE_STATUS_DATA_IN_USE for the usage status. + :param item_data: Dictionary containing the basic item data as would be required for a `ItemPostSchema` but with + mandatory IDs missing and any `id`'s replaced by the `name` value in its properties as the IDs + will be added automatically. + :return: ID of the created item (or `None` if not successful). + """ -def _post_catalogue_category_with_units(test_client, catalogue_category): - """Utility function for posting Catalogue category a defined at the top of this file""" + self.catalogue_item_id = self.post_catalogue_item_and_prerequisites_with_properties( + CATALOGUE_ITEM_DATA_WITH_ALL_PROPERTIES + ) + self.post_system(SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY) + self.post_usage_status(USAGE_STATUS_DATA_IN_USE) + + return self.post_item(item_data) + + def post_item_and_prerequisites_with_given_properties( + self, + catalogue_category_properties_data: list[dict], + catalogue_item_properties_data: list[dict], + item_properties_data: list[dict], + ) -> Optional[str]: + """ + Utility method that posts an item with specific given properties and also its prerequisite system, usage status, + catalogue item and catalogue category. Uses BASE_CATALOGUE_CATEGORY_DATA_WITH_PROPERTIES_MM and + ITEM_DATA_WITH_ALL_PROPERTIES as a base. + + :param catalogue_category_properties_data: List of dictionaries containing the basic catalogue category property + data as would be required for a `CatalogueCategoryPostPropertySchema` but with any `unit_id`'s + replaced by the `unit` value in its properties as the IDs will be added automatically. + :param catalogue_item_properties_data: List of dictionaries containing the basic catalogue item property data as + would be required for a `PropertyPostSchema` but with any `id`'s replaced by the `name` value as + the IDs will be added automatically. + :param item_properties_data: List of dictionaries containing the basic item property data as would be required + for a `PropertyPostSchema` but with any `id`'s replaced by the `name` value as the + IDs will be added automatically. + :return: ID of the created item (or `None` if not successful). + """ + + self.catalogue_item_id = self.post_catalogue_item_and_prerequisites_with_given_properties( + catalogue_category_properties_data, catalogue_item_properties_data + ) + self.post_system(SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY) + self.post_usage_status(USAGE_STATUS_DATA_IN_USE) + + return self.post_item({**ITEM_DATA_WITH_ALL_PROPERTIES, "properties": item_properties_data}) + + def post_item_and_prerequisites_with_allowed_values( + self, + property_type: str, + allowed_values_post_data: dict, + catalogue_item_property_value: Any, + item_property_value: Any, + ) -> Optional[str]: + """ + Utility method that posts an item with a property named 'property' of a given type with a given set of + allowed values as well as any prerequisite entities (a catalogue item, system and usage status) + + :param property_type: Type of the property to post. + :param allowed_values_post_data: Dictionary containing the allowed values data as would be required for an + `AllowedValuesSchema` to be posted with the catalogue category. + :param catalogue_item_property_value: Value of the property to post for the catalogue item. + :param item_property_value: Value of the property to post for the item. + :return: ID of the created item (or `None` if not successful). + """ + + self.catalogue_item_id = self.post_catalogue_item_and_prerequisites_with_allowed_values( + property_type, allowed_values_post_data, catalogue_item_property_value + ) + self.post_system(SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY) + self.post_usage_status(USAGE_STATUS_DATA_IN_USE) + return self.post_item( + { + **ITEM_DATA_REQUIRED_VALUES_ONLY, + "properties": [{"name": "property", "value": item_property_value}], + } + ) - units = _post_units(test_client) - - response = test_client.post( - "/v1/catalogue-categories", - json={ - **catalogue_category, - "properties": replace_unit_values_with_ids_in_properties(catalogue_category["properties"], units), - }, - ) - catalogue_category = response.json() + def check_post_item_success(self, expected_item_get_data: dict) -> None: + """ + Checks that a prior call to `post_item` gave a successful response with the expected data returned. - return catalogue_category + Also merges in any properties that were defined in the catalogue item but are not given in the expected data. + :param expected_item_get_data: Dictionary containing the expected item data returned as would be required for a + `ItemSchema`. Does not need mandatory IDs (e.g. `system_id`) as they will be + added automatically to check they are as expected. + """ -# pylint: enable=duplicate-code - + # Where properties are involved in the catalogue item, need to merge them + expected_item_get_data = self.merge_properties_in_expected_item_get_data(expected_item_get_data) -def test_create_item(test_client): - """ - Test creating an item. - """ - - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - # pylint: disable=duplicate-code - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - # pylint: enable=duplicate-code - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - # pylint: enable=duplicate-code - - assert response.status_code == 201 - - item = response.json() - - assert item == { - **ITEM_POST_EXPECTED, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": add_ids_to_properties( - catalogue_category["properties"], - ITEM_POST_EXPECTED["properties"], - ), - } - - -def test_create_item_with_invalid_catalogue_item_id(test_client): - """ - Test creating an item with an invalid catalogue item ID. - """ - item_post = { - **ITEM_POST, - "catalogue_item_id": "invalid", - "system_id": str(ObjectId()), - "usage_status_id": str(ObjectId()), - "properties": add_ids_to_properties(None, ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - assert response.status_code == 422 - assert response.json()["detail"] == "The specified catalogue item does not exist" - - -def test_create_item_with_non_existent_catalogue_item_id(test_client): - """ - Test creating an item with a non-existent catalogue item ID. - """ - item_post = { - **ITEM_POST, - "catalogue_item_id": str(ObjectId()), - "system_id": str(ObjectId()), - "usage_status_id": str(ObjectId()), - "properties": add_ids_to_properties(None, ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - assert response.status_code == 422 - assert response.json()["detail"] == "The specified catalogue item does not exist" - - -def test_create_item_with_invalid_system_id(test_client): - """ - Test creating an item with an invalid system ID. - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - # pylint: disable=duplicate-code - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - # pylint: enable=duplicate-code - - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": "invalid", - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - assert response.status_code == 422 - assert response.json()["detail"] == "The specified system does not exist" - - -def test_create_item_with_non_existent_system_id(test_client): - """ - Test creating an item with a non-existent system ID. - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - # pylint: disable=duplicate-code - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - # pylint: enable=duplicate-code - - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": str(ObjectId()), - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - assert response.status_code == 422 - assert response.json()["detail"] == "The specified system does not exist" - - -def test_create_with_missing_existing_properties(test_client): - """Test creating an item when not all properties defined in the catalogue item are supplied""" - - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - [ - {"name": "Property B", "value": False}, - {"name": "Property C", "value": "25x10x5"}, - ], - ), - } - # pylint: enable=duplicate-code - response = test_client.post("/v1/items", json=item_post) - - assert response.status_code == 201 - - item = response.json() - - assert item == { - **ITEM_POST_EXPECTED, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": add_ids_to_properties( - catalogue_category["properties"], - [ - {"name": "Property A", "value": 20, "unit": "mm"}, - {"name": "Property B", "unit": None, "value": False}, - {"name": "Property C", "unit": "cm", "value": "25x10x5"}, - {"name": "Property D", "unit": None, "value": None}, - ], - ), - } - - -def test_create_with_mandatory_properties_given_none(test_client): - """ - Test creating an item when a mandatory property is given a value of None - """ - - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - [ - {"name": "Property B", "value": None}, - {"name": "Property C", "value": None}, - ], - ), - } - # pylint: enable=duplicate-code - response = test_client.post("/v1/items", json=item_post) - - # pylint: disable=duplicate-code - assert response.status_code == 422 - prop_id = catalogue_category["properties"][1]["id"] - assert response.json()["detail"] == f"Mandatory property with ID '{prop_id}' cannot be None." - # pylint: enable=duplicate-code - - -def test_create_with_non_mandatory_properties_given_none(test_client): - """ - Test creating an item when non-mandatory properties are given a value of None - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - [ - {"name": "Property A", "value": None}, - {"name": "Property B", "value": False}, - {"name": "Property C", "value": "25x10x5"}, - {"name": "Property D", "value": None}, - ], - ), - } - # pylint: enable=duplicate-code - response = test_client.post("/v1/items", json=item_post) - - assert response.status_code == 201 - item = response.json() - assert item == { - **ITEM_POST_EXPECTED, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": add_ids_to_properties( - catalogue_category["properties"], - [ - {"name": "Property A", "unit": "mm", "value": None}, - {"name": "Property B", "unit": None, "value": False}, - {"name": "Property C", "unit": "cm", "value": "25x10x5"}, - {"name": "Property D", "unit": None, "value": None}, - ], - ), - } - - -def test_create_item_without_properties(test_client): - """ - Testing creating an item without properties. - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - # pylint: disable=duplicate-code - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - } - # pylint: enable=duplicate-code - del item_post["properties"] - response = test_client.post("/v1/items", json=item_post) - - assert response.status_code == 201 - - item = response.json() - - assert item == { - **ITEM_POST_EXPECTED, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": add_ids_to_properties( - catalogue_category["properties"], - [{"name": "Property A", "value": 20, "unit": "mm"}] + ITEM_POST_EXPECTED["properties"][-3:], - ), - } - - -def test_create_item_with_invalid_usage_status_id(test_client): - """ - Test creating an item with invalid usage status. - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - # pylint: disable=duplicate-code - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - # pylint: enable=duplicate-code - - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": "Invalid", - "properties": add_ids_to_properties( - catalogue_category["properties"], - [{"name": "Property C", "value": True}], - ), - } - response = test_client.post("/v1/items", json=item_post) - - assert response.status_code == 422 - assert response.json()["detail"] == "The specified usage status does not exist" - - -def test_create_item_with_non_existent_usage_status_id(test_client): - """ - Test creating an item with non existent usage status. - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - # pylint: disable=duplicate-code - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - # pylint: enable=duplicate-code - - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": str(ObjectId()), - "properties": add_ids_to_properties( - catalogue_category["properties"], - [{"name": "Property C", "value": True}], - ), - } - response = test_client.post("/v1/items", json=item_post) - - assert response.status_code == 422 - assert response.json()["detail"] == "The specified usage status does not exist" - - -def test_create_item_with_invalid_value_type_for_string_property(test_client): - """ - Test creating an item with invalid value type for a string property. - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - # pylint: disable=duplicate-code - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - # pylint: enable=duplicate-code - - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - [{"name": "Property C", "value": True}], - ), - } - response = test_client.post("/v1/items", json=item_post) - - # pylint: disable=duplicate-code - assert response.status_code == 422 - prop_id = catalogue_category["properties"][2]["id"] - assert response.json()["detail"] == f"Invalid value type for property with ID '{prop_id}'. Expected type: string." - # pylint: enable=duplicate-code - - -def test_create_item_with_invalid_value_type_for_number_property(test_client): - """ - Test creating an item with invalid value type for a number property. - """ - # pylint: disable=duplicate-code - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - # pylint: enable=duplicate-code - - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - [{"name": "Property A", "value": "20"}], - ), - } - response = test_client.post("/v1/items", json=item_post) - - # pylint: disable=duplicate-code - assert response.status_code == 422 - prop_id = catalogue_category["properties"][0]["id"] - assert response.json()["detail"] == f"Invalid value type for property with ID '{prop_id}'. Expected type: number." - # pylint: enable=duplicate-code - - -def test_create_item_with_invalid_value_type_for_boolean_property(test_client): - """ - Test creating an item with invalid value type for a boolean property. - """ - # pylint: disable=duplicate-code - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - # pylint: enable=duplicate-code - - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - [{"name": "Property B", "value": "False"}], - ), - } - response = test_client.post("/v1/items", json=item_post) - - # pylint: disable=duplicate-code - assert response.status_code == 422 - prop_id = catalogue_category["properties"][1]["id"] - assert response.json()["detail"] == f"Invalid value type for property with ID '{prop_id}'. Expected type: boolean." - # pylint: enable=duplicate-code - - -def test_create_item_with_allowed_values(test_client): - """ - Test creating an item when using allowed_values in the properties. - """ - # pylint: disable=duplicate-code - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_ALLOWED_VALUES) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - # pylint: enable=duplicate-code - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - # pylint: disable=duplicate-code - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_ALLOWED_VALUES, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_ALLOWED_VALUES["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - # pylint: enable=duplicate-code - - item_post = { - **ITEM_POST_ALLOWED_VALUES, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - ITEM_POST_ALLOWED_VALUES["properties"], - ), - } - response = test_client.post("/v1/items", json=item_post) - - assert response.status_code == 201 - - item = response.json() - - assert item == { - **ITEM_POST_ALLOWED_VALUES_EXPECTED, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": add_ids_to_properties( - catalogue_category["properties"], - ITEM_POST_ALLOWED_VALUES_EXPECTED["properties"], - ), - } - - -def test_create_item_with_allowed_values_invalid_list_string(test_client): - """ - Test creating an item when giving a string property a value that is not within the defined allowed_values - list - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_ALLOWED_VALUES) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - # pylint: disable=duplicate-code - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_ALLOWED_VALUES, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_ALLOWED_VALUES["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - # pylint: enable=duplicate-code - - item_post = { - **ITEM_POST_ALLOWED_VALUES, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - [ - {"name": "Property A", "value": 4}, - {"name": "Property B", "value": "blue"}, - ], - ), - } - response = test_client.post("/v1/items", json=item_post) - - # pylint: disable=duplicate-code - assert response.status_code == 422 - prop_id = catalogue_category["properties"][1]["id"] - assert response.json()["detail"] == f"Invalid value for property with ID '{prop_id}'. Expected one of red, green." - # pylint: enable=duplicate-code - - -def test_create_item_with_allowed_values_invalid_list_number(test_client): - """ - Test creating an item when giving a number property a value that is not within the defined allowed_values - list - """ - # pylint: disable=duplicate-code - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_ALLOWED_VALUES) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_ALLOWED_VALUES, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_ALLOWED_VALUES["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - # pylint: enable=duplicate-code - - item_post = { - **ITEM_POST_ALLOWED_VALUES, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - [ - {"name": "Property A", "value": 10}, - {"name": "Property B", "value": "red"}, - ], - ), - } - response = test_client.post("/v1/items", json=item_post) - - # pylint: disable=duplicate-code - assert response.status_code == 422 - prop_id = catalogue_category["properties"][0]["id"] - assert response.json()["detail"] == f"Invalid value for property with ID '{prop_id}'. Expected one of 2, 4, 6." - # pylint: enable=duplicate-code - - -def test_delete(test_client): - """ - Test deleting an item - """ - # pylint: disable=duplicate-code - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - - # pylint: enable=duplicate-code - response = test_client.post("/v1/items", json=item_post) - - item_id = response.json()["id"] - - response = test_client.delete(f"/v1/items/{item_id}") - - assert response.status_code == 204 - response = test_client.delete(f"/v1/items/{item_id}") - assert response.status_code == 404 - - -def test_delete_with_invalid_id(test_client): - """ - Test deleting an item with an invalid ID. - """ - response = test_client.delete("v1/items/invalid") - - assert response.status_code == 404 - assert response.json()["detail"] == "Item not found" - - -def test_delete_catalogue_item_with_non_existent_id(test_client): - """ - Test deleting an item with a non-existent ID. - """ - response = test_client.delete(f"/v1/items/{str(ObjectId())}") - - assert response.status_code == 404 - assert response.json()["detail"] == "Item not found" - - -def test_get_item(test_client): - """ - Test getting an item by its ID. - """ - - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - # pylint: disable=duplicate-code - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - # pylint: enable=duplicate-code - - response = test_client.post("/v1/items", json=item_post) - - item_id = response.json()["id"] - response = test_client.get(f"/v1/items/{item_id}") - - assert response.status_code == 200 - - item = response.json() - - assert item == { - **ITEM_POST_EXPECTED, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": add_ids_to_properties( - catalogue_category["properties"], - ITEM_POST_EXPECTED["properties"], - ), - } - - -def test_get_item_with_non_existent_id(test_client): - """ - Test getting an item with a non-existent id - """ - response = test_client.get(f"/v1/items/{str(ObjectId())}") - - assert response.status_code == 404 - assert response.json()["detail"] == "An item with such ID was not found" - - -def test_get_item_with_invalid_id(test_client): - """ - Test getting an item with an invalid id - """ - response = test_client.get("/v1/items/invalid") - - assert response.status_code == 404 - assert response.json()["detail"] == "An item with such ID was not found" - - -def test_get_items(test_client): - """ - Test getting items - """ - - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id_a = response.json()["id"] - response = test_client.post("/v1/systems", json=SYSTEM_POST_B) - system_id_b = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - # pylint: disable=duplicate-code - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - # pylint: enable=duplicate-code - - item_post_a = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id_a, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - - item_post_b = {**item_post_a, "system_id": system_id_b} - - test_client.post("/v1/items", json=item_post_a) - test_client.post("/v1/items", json=item_post_b) - - response = test_client.get("/v1/items") - - assert response.status_code == 200 - - items = response.json() - - properties_expected = add_ids_to_properties( - catalogue_category["properties"], - ITEM_POST_EXPECTED["properties"], - ) - assert items == [ - { - **ITEM_POST_EXPECTED, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id_a, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": properties_expected, - }, - { - **ITEM_POST_EXPECTED, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id_b, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": properties_expected, - }, - ] - - -def test_get_items_with_system_id_filters(test_client): - """ - Test getting items with system id filter - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - # pylint: disable=duplicate-code - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - # pylint: enable=duplicate-code - - item_post_a = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": None, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - - item_post_b = {**item_post_a, "system_id": system_id} - - test_client.post("/v1/items", json=item_post_a) - test_client.post("/v1/items", json=item_post_b) - - response = test_client.get("/v1/items", params={"system_id": system_id}) - - assert response.status_code == 200 - - items = response.json() - - assert items == [ - { - **ITEM_POST_EXPECTED, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": add_ids_to_properties( - catalogue_category["properties"], - ITEM_POST_EXPECTED["properties"], - ), - } - ] - - -def test_get_items_with_catalogue_id_filters(test_client): - """ - Test getting items with catalogue item id filter - """ - - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - # pylint: disable=duplicate-code - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - # pylint: enable=duplicate-code - - # pylint: disable=duplicate-code - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - test_client.post("/v1/items", json=item_post) - # pylint: enable=duplicate-code - - response = test_client.get("/v1/items", params={"catalogue_item_id": catalogue_item_id}) - - assert response.status_code == 200 - - items = response.json() - - assert items == [ - { - **ITEM_POST_EXPECTED, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": add_ids_to_properties( - catalogue_category["properties"], - ITEM_POST_EXPECTED["properties"], - ), - } - ] + assert self._post_response_item.status_code == 201 + assert self._post_response_item.json() == self.add_ids_to_expected_item_get_data(expected_item_get_data) + def check_post_item_failed_with_detail(self, status_code: int, detail: str) -> None: + """ + Checks that a prior call to `post_item` gave a failed response with the expected code and error message. -def test_get_items_with_no_matching_filters(test_client): - """ - Test getting items with neither filter having matching results - """ - - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - # pylint: disable=duplicate-code - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - # pylint: enable=duplicate-code - - # pylint: disable=duplicate-code - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - # pylint: enable=duplicate-code - - test_client.post("/v1/items", json=item_post) - test_client.post("/v1/items", json=item_post) - - response = test_client.get( - "/v1/items", - params={"system_id": str(ObjectId()), "catalogue_item_id": str(ObjectId())}, - ) - - assert response.status_code == 200 - - items = response.json() - - assert not items + :param status_code: Expected status code of the response. + :param detail: Expected detail given in the response. + """ + assert self._post_response_item.status_code == status_code + assert self._post_response_item.json()["detail"] == detail -def test_get_items_with_invalid_system_id_filter(test_client): - """ - Test getting items with an invalid system id filter - """ - response = test_client.get("/v1/items", params={"system_id": "Invalid"}) + def check_post_item_failed_with_validation_message(self, status_code: int, message: str) -> None: + """ + Checks that a prior call to `post_item` gave a failed response with the expected code and pydantic validation + error message. - assert response.status_code == 200 - assert response.json() == [] + :param status_code: Expected status code of the response. + :param message: Expected validation error message given in the response. + """ + assert self._post_response_item.status_code == status_code + assert self._post_response_item.json()["detail"][0]["msg"] == message -def test_get_items_with_invalid_catalogue_item_id_filter(test_client): - """ - Test getting items with an invalid catalogue item id filter - """ - response = test_client.get("/v1/items", params={"catalogue_item_id": "Invalid"}) - assert response.status_code == 200 - assert response.json() == [] +class TestCreate(CreateDSL): + """Tests for creating an item.""" + def test_create_with_only_required_values_provided(self): + """Test creating an item with only required values provided.""" -def test_partial_update_item(test_client): - """ - Test changing 'usage_status' and 'is_defective' in an item - """ + self.post_item_and_prerequisites_no_properties(ITEM_DATA_REQUIRED_VALUES_ONLY) - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] + self.check_post_item_success(ITEM_GET_DATA_REQUIRED_VALUES_ONLY) - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = {"usage_status": "Used", "is_defective": True} - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - assert response.status_code == 200 - - item = response.json() - - assert item == { - **ITEM_POST_EXPECTED, - **item_patch, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": add_ids_to_properties( - catalogue_category["properties"], - ITEM_POST_EXPECTED["properties"], - ), - } - - -def test_partial_update_item_invalid_id(test_client): - """ - Test updating an item with an invalid ID. - """ - - item_patch = {"usage_status_id": str(ObjectId()), "is_defective": True} - - response = test_client.patch("/v1/items/invalid", json=item_patch) - - assert response.status_code == 404 - assert response.json()["detail"] == "Item not found" - - -def test_partial_update_item_non_existent_id(test_client): - """ - Test updating an item with a non-existent ID. - """ - item_patch = {"usage_status_id": str(ObjectId()), "is_defective": True} - - response = test_client.patch(f"/v1/items/{str(ObjectId())}", json=item_patch) - - assert response.status_code == 404 - assert response.json()["detail"] == "Item not found" - - -def test_partial_update_change_catalogue_item_id(test_client): - """ - Test moving an item to another catalogue item - """ - - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = {"catalogue_item_id": str(ObjectId())} - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - assert response.status_code == 422 - assert response.json()["detail"] == "Cannot change the catalogue item of an item" - - -def test_partial_update_change_system_id(test_client): - """ - Test changing the system ID of an item - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id_a = response.json()["id"] - - response = test_client.post("/v1/systems", json=SYSTEM_POST_B) - system_id_b = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id_a, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = {"system_id": system_id_b} - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - assert response.status_code == 200 - - item = response.json() - - assert item == { - **ITEM_POST_EXPECTED, - **item_patch, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id_b, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": add_ids_to_properties( - catalogue_category["properties"], - ITEM_POST_EXPECTED["properties"], - ), - } - - -def test_partial_update_change_non_existent_system_id(test_client): - """ - Test updating system ID which is non-existent - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = {"system_id": str(ObjectId())} - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - assert response.status_code == 422 - assert response.json()["detail"] == "The specified system does not exist" - - -def test_partial_update_change_non_existent_usage_status_id(test_client): - """ - Test updating usage status ID which is non-existent - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = {"usage_status_id": str(ObjectId())} - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - assert response.status_code == 422 - assert response.json()["detail"] == "The specified usage status does not exist" - - -def test_partial_update_change_invalid_usage_status_id(test_client): - """ - Test updating usage status ID which is invalid - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = {"usage_status_id": "invalid"} - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - assert response.status_code == 422 - assert response.json()["detail"] == "The specified usage status does not exist" - - -def test_partial_update_property_values(test_client): - """ - Test updating property values - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = { - "properties": add_ids_to_properties( - catalogue_category["properties"], - [ - {"name": "Property A", "value": 12}, - {"name": "Property B", "value": False}, - {"name": "Property C", "value": "20x15x10"}, - ], - ), - } - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - assert response.status_code == 200 - - item = response.json() - - assert item == { - **ITEM_POST_EXPECTED, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": add_ids_to_properties( - catalogue_category["properties"], - [{"name": "Property A", "value": 12, "unit": "mm"}] + ITEM_POST_EXPECTED["properties"][-3:], - ), - } - - -def test_partial_update_property_values_with_mandatory_properties_given_none( - test_client, -): - """ - Test updating a item's mandatory properties to have a value of None - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = { - "properties": add_ids_to_properties( - catalogue_category["properties"], - [ - {"name": "Property B", "value": None}, - {"name": "Property C", "value": None}, - ], - ), - } - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - # pylint: disable=duplicate-code - assert response.status_code == 422 - prop_id = catalogue_category["properties"][1]["id"] - assert response.json()["detail"] == f"Mandatory property with ID '{prop_id}' cannot be None." - # pylint: enable=duplicate-code - - -def test_partial_update_property_values_with_non_mandatory_properties_given_none( - test_client, -): - """ - Test updating a item's mandatory properties to have a value of None - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = { - "properties": add_ids_to_properties( - catalogue_category["properties"], - [ - {"name": "Property A", "value": None}, - {"name": "Property B", "value": False}, - {"name": "Property C", "value": "20x15x10"}, - {"name": "Property D", "value": None}, - ], - ), - } - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - assert response.status_code == 200 - - item = response.json() - - assert item == { - **ITEM_POST_EXPECTED, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": add_ids_to_properties( - catalogue_category["properties"], - [ - {"name": "Property A", "unit": "mm", "value": None}, - {"name": "Property B", "unit": None, "value": False}, - {"name": "Property C", "unit": "cm", "value": "20x15x10"}, - {"name": "Property D", "unit": None, "value": None}, + def test_create_with_all_values_except_properties(self): + """Test creating an item with all values provided except `properties`.""" + + self.post_item_and_prerequisites_no_properties(ITEM_DATA_ALL_VALUES_NO_PROPERTIES) + + self.check_post_item_success(ITEM_GET_DATA_ALL_VALUES_NO_PROPERTIES) + + def test_create_with_no_properties_provided(self): + """Test creating an item when none of the properties present in the catalogue item are defined in the item.""" + + self.post_item_and_prerequisites_with_properties(ITEM_DATA_REQUIRED_VALUES_ONLY) + + self.check_post_item_success(ITEM_GET_DATA_REQUIRED_VALUES_ONLY) + + def test_create_with_some_properties_provided(self): + """Test creating an item when only some properties present in the catalogue item are defined in the item.""" + + self.post_item_and_prerequisites_with_properties( + {**ITEM_DATA_WITH_ALL_PROPERTIES, "properties": ITEM_DATA_WITH_ALL_PROPERTIES["properties"][1::]} + ) + + self.check_post_item_success( + {**ITEM_GET_DATA_WITH_ALL_PROPERTIES, "properties": ITEM_GET_DATA_WITH_ALL_PROPERTIES["properties"][1::]} + ) + + def test_create_with_all_properties_provided(self): + """Test creating an item when all properties present in the catalogue item are defined in the item.""" + + self.post_item_and_prerequisites_with_properties(ITEM_DATA_WITH_ALL_PROPERTIES) + + self.check_post_item_success(ITEM_GET_DATA_WITH_ALL_PROPERTIES) + + def test_create_with_mandatory_property_given_none(self): + """Test creating an item when a mandatory property is given a value of `None` in the item.""" + + self.post_item_and_prerequisites_with_properties( + {**ITEM_DATA_WITH_ALL_PROPERTIES, "properties": [{**PROPERTY_DATA_BOOLEAN_MANDATORY_FALSE, "value": None}]} + ) + + self.check_post_item_failed_with_detail( + 422, + f"Mandatory property with ID '{self.property_name_id_dict[PROPERTY_DATA_BOOLEAN_MANDATORY_FALSE['name']]}' " + "cannot be None.", + ) + + def test_create_with_non_mandatory_property_given_none(self): + """Test creating an item when a non mandatory property is given a value of `None` in the item.""" + + self.post_item_and_prerequisites_with_properties( + { + **ITEM_DATA_WITH_ALL_PROPERTIES, + "properties": [{**PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT_1, "value": None}], + } + ) + + self.check_post_item_success( + { + **ITEM_GET_DATA_WITH_ALL_PROPERTIES, + "properties": [{**PROPERTY_GET_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT_1, "value": None}], + } + ) + + def test_create_with_string_property_with_invalid_value_type(self): + """Test creating an item with an invalid value type for a string property.""" + + self.post_item_and_prerequisites_with_given_properties( + catalogue_category_properties_data=[CATALOGUE_CATEGORY_PROPERTY_DATA_STRING_MANDATORY], + catalogue_item_properties_data=[PROPERTY_DATA_STRING_MANDATORY_TEXT], + item_properties_data=[{**PROPERTY_DATA_STRING_MANDATORY_TEXT, "value": 42}], + ) + + self.check_post_item_failed_with_detail( + 422, + "Invalid value type for property with ID " + f"'{self.property_name_id_dict[CATALOGUE_CATEGORY_PROPERTY_DATA_STRING_MANDATORY['name']]}'. " + "Expected type: string.", + ) + + def test_create_with_number_property_with_invalid_value_type(self): + """Test creating an item with an invalid value type for a number property.""" + + self.post_item_and_prerequisites_with_given_properties( + catalogue_category_properties_data=[CATALOGUE_CATEGORY_PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT], + catalogue_item_properties_data=[PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT_1], + item_properties_data=[{**PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT_1, "value": "42"}], + ) + + self.check_post_item_failed_with_detail( + 422, + "Invalid value type for property with ID '" + f"{self.property_name_id_dict[CATALOGUE_CATEGORY_PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT['name']]}" + "'. Expected type: number.", + ) + + def test_create_with_boolean_property_with_invalid_value_type(self): + """Test creating an item with an invalid value type for a boolean property.""" + + self.post_item_and_prerequisites_with_given_properties( + catalogue_category_properties_data=[CATALOGUE_CATEGORY_PROPERTY_DATA_BOOLEAN_MANDATORY], + catalogue_item_properties_data=[PROPERTY_DATA_BOOLEAN_MANDATORY_FALSE], + item_properties_data=[{**PROPERTY_DATA_BOOLEAN_MANDATORY_FALSE, "value": 0}], + ) + + self.check_post_item_failed_with_detail( + 422, + "Invalid value type for property with ID '" + f"{self.property_name_id_dict[CATALOGUE_CATEGORY_PROPERTY_DATA_BOOLEAN_MANDATORY['name']]}" + "'. Expected type: boolean.", + ) + + def test_create_with_allowed_values_list(self): + """Test creating an item with properties that have allowed values lists.""" + + self.post_item_and_prerequisites_with_given_properties( + catalogue_category_properties_data=[ + CATALOGUE_CATEGORY_PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST, + CATALOGUE_CATEGORY_PROPERTY_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST, ], - ), - } - - -def test_partial_update_property_values_with_allowed_values(test_client): - """ - Test updating property values when using allowed_values in the catalogue category properties - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_ALLOWED_VALUES) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_ALLOWED_VALUES, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_ALLOWED_VALUES["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST_ALLOWED_VALUES, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - ITEM_POST_ALLOWED_VALUES["properties"], - ), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = { - "properties": add_ids_to_properties( - catalogue_category["properties"], - [ - {"name": "Property A", "value": 2}, - {"name": "Property B", "value": "red"}, + catalogue_item_properties_data=[ + PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_1, + PROPERTY_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE1, ], - ), - } - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - assert response.status_code == 200 - - item = response.json() - - assert item == { - **ITEM_POST_EXPECTED, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": add_ids_to_properties( - catalogue_category["properties"], - [ - {"name": "Property A", "unit": "mm", "value": 2}, - {"name": "Property B", "value": "red", "unit": None}, + item_properties_data=[ + PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_2, + PROPERTY_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE2, ], - ), - } - - -def test_partial_update_property_values_with_allowed_values_invalid_list_string( - test_client, -): - """ - Test updating property values when giving a string property a value that is not within the defined - allowed_values - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_ALLOWED_VALUES) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_ALLOWED_VALUES, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_ALLOWED_VALUES["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST_ALLOWED_VALUES, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - ITEM_POST_ALLOWED_VALUES["properties"], - ), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = { - "properties": add_ids_to_properties( - catalogue_category["properties"], - [{"name": "Property B", "value": "blue"}], - ), - } - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - # pylint: disable=duplicate-code - assert response.status_code == 422 - prop_id = catalogue_category["properties"][1]["id"] - assert response.json()["detail"] == f"Invalid value for property with ID '{prop_id}'. Expected one of red, green." - # pylint: enable=duplicate-code - - -def test_partial_update_property_values_with_allowed_values_invalid_list_number( - test_client, -): - """ - Test updating property values when giving a number property a value that is not within the defined - allowed_values - """ - # pylint: disable=duplicate-code - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_ALLOWED_VALUES) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_ALLOWED_VALUES, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_ALLOWED_VALUES["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST_ALLOWED_VALUES, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - ITEM_POST_ALLOWED_VALUES["properties"], - ), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = { - "properties": add_ids_to_properties( - catalogue_category["properties"], - [{"name": "Property A", "value": 10}], - ), - } - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - # pylint: disable=duplicate-code - assert response.status_code == 422 - prop_id = catalogue_category["properties"][0]["id"] - assert response.json()["detail"] == f"Invalid value for property with ID '{prop_id}'. Expected one of 2, 4, 6." - # pylint: enable=duplicate-code - - -def test_partial_update_with_missing_existing_properties(test_client): - """ - Test updating an item when not all properties defined in the catalogue item are supplied - """ - # pylint: disable=duplicate-code - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = { - "properties": add_ids_to_properties( - catalogue_category["properties"], - [ - {"name": "Property B", "value": False}, - {"name": "Property C", "value": "25x10x5"}, + ) + self.check_post_item_success( + { + **ITEM_GET_DATA_WITH_ALL_PROPERTIES, + "properties": [ + PROPERTY_GET_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_2, + PROPERTY_GET_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE2, + ], + } + ) + + def test_create_with_string_property_with_allowed_values_list_with_invalid_value(self): + """Test creating an item with a string property with an allowed values list while giving it a value not in the + list.""" + + self.post_item_and_prerequisites_with_allowed_values( + "string", {"type": "list", "values": ["value1", "value2", "value3"]}, "value1", "value42" + ) + self.check_post_item_failed_with_detail( + 422, + f"Invalid value for property with ID '{self.property_name_id_dict['property']}'. " + "Expected one of value1, value2, value3.", + ) + + def test_create_with_string_property_with_allowed_values_list_with_invalid_type(self): + """Test creating an item with a string property with an allowed values list while giving it a value with an + incorrect type.""" + + self.post_item_and_prerequisites_with_allowed_values( + "string", {"type": "list", "values": ["value1", "value2", "value3"]}, "value1", 42 + ) + self.check_post_item_failed_with_detail( + 422, + f"Invalid value type for property with ID '{self.property_name_id_dict['property']}'. " + "Expected type: string.", + ) + + def test_create_with_number_property_with_allowed_values_list_with_invalid_value(self): + """Test creating an item with a number property with an allowed values list while giving it a value not in the + list.""" + + self.post_item_and_prerequisites_with_allowed_values("number", {"type": "list", "values": [1, 2, 3]}, 1, 42) + self.check_post_item_failed_with_detail( + 422, + f"Invalid value for property with ID '{self.property_name_id_dict['property']}'. Expected one of 1, 2, 3.", + ) + + def test_create_with_number_property_with_allowed_values_list_with_invalid_type(self): + """Test creating an item with a number property with an allowed values list while giving it a value with an + incorrect type.""" + + self.post_item_and_prerequisites_with_allowed_values("number", {"type": "list", "values": [1, 2, 3]}, 1, "test") + self.check_post_item_failed_with_detail( + 422, + f"Invalid value type for property with ID '{self.property_name_id_dict['property']}'. " + "Expected type: number.", + ) + + def test_create_with_non_existent_catalogue_item_id(self): + """Test creating an item with a non-existent catalogue item ID.""" + + self.catalogue_item_id = str(ObjectId()) + self.post_system(SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY) + self.post_usage_status(USAGE_STATUS_DATA_IN_USE) + self.post_item(ITEM_DATA_REQUIRED_VALUES_ONLY) + + self.check_post_item_failed_with_detail(422, "The specified catalogue item does not exist") + + def test_create_with_invalid_catalogue_item_id(self): + """Test creating an item with an invalid catalogue item ID.""" + + self.catalogue_item_id = "invalid-id" + self.post_system(SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY) + self.post_usage_status(USAGE_STATUS_DATA_IN_USE) + self.post_item(ITEM_DATA_REQUIRED_VALUES_ONLY) + + self.check_post_item_failed_with_detail(422, "The specified catalogue item does not exist") + + def test_create_with_non_existent_system_id(self): + """Test creating an item with a non-existent system ID.""" + + self.catalogue_item_id = self.post_catalogue_item_and_prerequisites_no_properties( + CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY + ) + self.system_id = str(ObjectId()) + self.post_usage_status(USAGE_STATUS_DATA_IN_USE) + self.post_item(ITEM_DATA_REQUIRED_VALUES_ONLY) + + self.check_post_item_failed_with_detail(422, "The specified system does not exist") + + def test_create_with_invalid_system_id(self): + """Test creating an item with an invalid system ID.""" + + self.catalogue_item_id = self.post_catalogue_item_and_prerequisites_no_properties( + CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY + ) + self.system_id = "invalid-id" + self.post_usage_status(USAGE_STATUS_DATA_IN_USE) + self.post_item(ITEM_DATA_REQUIRED_VALUES_ONLY) + + self.check_post_item_failed_with_detail(422, "The specified system does not exist") + + def test_create_with_non_existent_usage_status_id(self): + """Test creating an item with a non-existent usage status ID.""" + + self.catalogue_item_id = self.post_catalogue_item_and_prerequisites_no_properties( + CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY + ) + self.post_system(SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY) + self.add_usage_status_value_and_id(ITEM_DATA_REQUIRED_VALUES_ONLY["usage_status"], str(ObjectId())) + self.post_item(ITEM_DATA_REQUIRED_VALUES_ONLY) + + self.check_post_item_failed_with_detail(422, "The specified usage status does not exist") + + def test_create_with_invalid_usage_status_id(self): + """Test creating an item with an invalid usage status ID.""" + + self.catalogue_item_id = self.post_catalogue_item_and_prerequisites_no_properties( + CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY + ) + self.post_system(SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY) + self.post_usage_status(USAGE_STATUS_DATA_IN_USE) + self.add_usage_status_value_and_id(ITEM_DATA_REQUIRED_VALUES_ONLY["usage_status"], "invalid-id") + self.post_item(ITEM_DATA_REQUIRED_VALUES_ONLY) + + self.check_post_item_failed_with_detail(422, "The specified usage status does not exist") + + +class GetDSL(CreateDSL): + """Base class for get tests.""" + + _get_response_item: Response + + def get_item(self, item_id: str) -> None: + """ + Gets an item with the given ID. + + :param item_id: ID of the item to be obtained. + """ + + self._get_response_item = self.test_client.get(f"/v1/items/{item_id}") + + def check_get_item_success(self, expected_item_get_data: dict) -> None: + """ + Checks that a prior call to `get_item` gave a successful response with the expected data returned. + + :param expected_item_get_data: Dictionary containing the expected item data returned as would be required for a + `ItemSchema`. Does not need mandatory IDs (e.g. `system_id`) as they will + be added automatically to check they are as expected. + """ + + assert self._get_response_item.status_code == 200 + assert self._get_response_item.json() == self.add_ids_to_expected_item_get_data(expected_item_get_data) + + def check_get_item_failed_with_detail(self, status_code: int, detail: str) -> None: + """ + Checks that a prior call to `get_item` gave a failed response with the expected code and error + message. + + :param status_code: Expected status code of the response. + :param detail: Expected detail given in the response. + """ + + assert self._get_response_item.status_code == status_code + assert self._get_response_item.json()["detail"] == detail + + +class TestGet(GetDSL): + """Tests for getting an item.""" + + def test_get(self): + """Test getting an item.""" + + catalogue_item_id = self.post_item_and_prerequisites_no_properties(ITEM_DATA_REQUIRED_VALUES_ONLY) + + self.get_item(catalogue_item_id) + self.check_get_item_success(ITEM_GET_DATA_REQUIRED_VALUES_ONLY) + + def test_get_with_non_existent_id(self): + """Test getting an item with a non-existent ID.""" + + self.get_item(str(ObjectId())) + self.check_get_item_failed_with_detail(404, "An item with such ID was not found") + + def test_get_with_invalid_id(self): + """Test getting an item with an invalid ID.""" + + self.get_item("invalid-id") + self.check_get_item_failed_with_detail(404, "An item with such ID was not found") + + +class ListDSL(GetDSL): + """Base class for list tests.""" + + def get_items(self, filters: dict) -> None: + """ + Gets a list of items with the given filters. + + :param filters: Filters to use in the request. + """ + + self._get_response_item = self.test_client.get("/v1/items", params=filters) + + def post_test_items(self) -> list[dict]: + """ + Posts three items. The first two have the same catalogue item but different systems, and the last has a + different catalogue item but the same system as the second catalogue item. + + :return: List of dictionaries containing the expected item data returned from a get endpoint in + the form of an `ItemSchema`. In the form [CATALOGUE_ITEM_A_SYSTEM_A, CATALOGUE_ITEM_A_SYSTEM_B, + CATALOGUE_ITEM_B_SYSTEM_B] + """ + + # First item + self.post_item_and_prerequisites_no_properties(ITEM_DATA_REQUIRED_VALUES_ONLY) + system_a_id = self.system_id + catalogue_item_a_id = self.catalogue_item_id + + # Second item + system_b_id = self.post_system({**SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY, "name": "Another system"}) + self.post_item(ITEM_DATA_ALL_VALUES_NO_PROPERTIES) + + # Third item + self.post_catalogue_item({**CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY, "name": "Another catalogue item"}) + catalogue_item_b_id = self.catalogue_item_id + self.post_item(ITEM_DATA_REQUIRED_VALUES_ONLY) + + return [ + { + **ITEM_GET_DATA_REQUIRED_VALUES_ONLY, + "catalogue_item_id": catalogue_item_a_id, + "system_id": system_a_id, + "usage_status_id": self.usage_status_value_id_dict[ITEM_DATA_REQUIRED_VALUES_ONLY["usage_status"]], + }, + { + **ITEM_GET_DATA_ALL_VALUES_NO_PROPERTIES, + "catalogue_item_id": catalogue_item_a_id, + "system_id": system_b_id, + "usage_status_id": self.usage_status_value_id_dict[ITEM_DATA_REQUIRED_VALUES_ONLY["usage_status"]], + }, + { + **ITEM_GET_DATA_REQUIRED_VALUES_ONLY, + "catalogue_item_id": catalogue_item_b_id, + "system_id": system_b_id, + "usage_status_id": self.usage_status_value_id_dict[ITEM_DATA_REQUIRED_VALUES_ONLY["usage_status"]], + }, + ] + + def check_get_items_success(self, expected_items_get_data: list[dict]) -> None: + """ + Checks that a prior call to `get_items` gave a successful response with the expected data returned. + + :param expected_items_get_data: List of dictionaries containing the expected item data returned as would be + required for `ItemSchema`'s. + """ + + assert self._get_response_item.status_code == 200 + assert self._get_response_item.json() == expected_items_get_data + + +class TestList(ListDSL): + """Tests for getting a list of items.""" + + def test_list_with_no_filters(self): + """ + Test getting a list of all items with no filters provided. + + Posts three items the first two in the same catalogue item and separate systems and the third in a different + catalogue item but the same system as the second. Expects all three to be returned. + """ + + items = self.post_test_items() + self.get_items(filters={}) + self.check_get_items_success(items) + + def test_list_with_system_id_filter(self): + """ + Test getting a list of all items with a `system_id` filter provided. + + Posts three items the first two in the same catalogue item and separate systems and the third in a different + catalogue item but the same system as the second. Expects just the latter two systems to be returned. + """ + + items = self.post_test_items() + self.get_items(filters={"system_id": items[1]["system_id"]}) + self.check_get_items_success(items[1:]) + + def test_list_with_invalid_system_id_filter(self): + """Test getting a list of all items with an invalid `system_id` filter provided.""" + + self.get_items(filters={"system_id": "invalid-id"}) + self.check_get_items_success([]) + + def test_list_with_catalogue_item_id_filter(self): + """ + Test getting a list of all items with a `catalogue_item_id` filter provided. + + Posts three items the first two in the same catalogue item and separate systems and the third in a different + catalogue item but the same system as the second. Expects just the former two systems to be returned. + """ + + items = self.post_test_items() + self.get_items(filters={"catalogue_item_id": items[0]["catalogue_item_id"]}) + self.check_get_items_success(items[0:2]) + + def test_list_with_invalid_catalogue_item_id_filter(self): + """Test getting a list of all items with an invalid `catalogue_item_id` filter provided.""" + + self.get_items(filters={"catalogue_item_id": "invalid-id"}) + self.check_get_items_success([]) + + def test_list_with_system_id_and_catalogue_item_id_filters(self): + """ + Test getting a list of all items with `system_id` and `catalogue_item_id` filters provided. + + Posts three items the first two in the same catalogue item and separate systems and the third in a different + catalogue item but the same system as the second. Expects just second item to be returned. + """ + + items = self.post_test_items() + self.get_items(filters={"system_id": items[2]["system_id"], "catalogue_item_id": items[0]["catalogue_item_id"]}) + self.check_get_items_success([items[1]]) + + def test_list_with_system_id_and_catalogue_item_id_filters_with_no_matching_results(self): + """ + Test getting a list of all items with `system_id` and `catalogue_item_id` filters provided when there are no + matching results. + """ + + self.get_items(filters={"system_id": str(ObjectId()), "catalogue_item_id": str(ObjectId())}) + self.check_get_items_success([]) + + +class UpdateDSL(ListDSL): + """Base class for update tests.""" + + _patch_response_item: Response + + def patch_item(self, item_id: str, item_update_data: dict) -> None: + """ + Updates an item with the given ID. + + :param item_id: ID of the item to patch. + :param item_update_data: Dictionary containing the basic patch data as would be required for a `ItemPatchSchema` + but with any `id`'s replaced by the `name` value in its properties as the IDs will be + added automatically. + """ + + # Replace any property names with ids + item_update_data = E2ETestHelpers.replace_property_names_with_ids_in_properties( + item_update_data, self.property_name_id_dict + ) + + self._patch_response_item = self.test_client.patch(f"/v1/items/{item_id}", json=item_update_data) + + def check_patch_item_response_success(self, expected_item_get_data: dict) -> None: + """ + Checks that a prior call to `patch_item` gave a successful response with the expected data returned. + + Also merges in any properties that were defined in the catalogue item but are not given in the expected data. + + :param expected_item_get_data: Dictionary containing the expected item data returned as would + be required for a `ItemSchema`. Does not need mandatory IDs + (e.g. `system_id`) as they will be added automatically to check + they are as expected. + """ + + # Where properties are involved in the catalogue item, need to merge them + expected_item_get_data = self.merge_properties_in_expected_item_get_data(expected_item_get_data) + + assert self._patch_response_item.status_code == 200 + assert self._patch_response_item.json() == self.add_ids_to_expected_item_get_data(expected_item_get_data) + + E2ETestHelpers.check_created_and_modified_times_updated_correctly( + self._post_response_item, self._patch_response_item + ) + + def check_patch_item_failed_with_detail(self, status_code: int, detail: str) -> None: + """ + Checks that a prior call to `patch_item` gave a failed response with the expected code and error message. + + :param status_code: Expected status code of the response. + :param detail: Expected detail given in the response. + """ + + assert self._patch_response_item.status_code == status_code + assert self._patch_response_item.json()["detail"] == detail + + def check_patch_item_failed_with_validation_message(self, status_code: int, message: str) -> None: + """ + Checks that a prior call to `patch_item` gave a failed response with the expected code and pydantic validation + error message. + + :param status_code: Expected status code of the response. + :param message: Expected validation error message given in the response. + """ + + assert self._patch_response_item.status_code == status_code + assert self._patch_response_item.json()["detail"][0]["msg"] == message + + +class TestUpdate(UpdateDSL): + """Tests for updating an item.""" + + def test_partial_update_all_fields_except_ids_or_properties(self): + """Test updating all fields of an item except any of its `_id` fields or properties.""" + + item_id = self.post_item_and_prerequisites_no_properties(ITEM_DATA_REQUIRED_VALUES_ONLY) + + self.patch_item(item_id, ITEM_DATA_ALL_VALUES_NO_PROPERTIES) + self.check_patch_item_response_success(ITEM_GET_DATA_ALL_VALUES_NO_PROPERTIES) + + def test_partial_update_catalogue_item_id(self): + """Test updating the `catalogue_item_id` of an item.""" + + item_id = self.post_item_and_prerequisites_no_properties(ITEM_DATA_REQUIRED_VALUES_ONLY) + + self.patch_item(item_id, {"catalogue_item_id": str(ObjectId())}) + self.check_patch_item_failed_with_detail(422, "Cannot change the catalogue item of an item") + + def test_partial_update_system_id(self): + """Test updating the `system_id` of an item.""" + + item_id = self.post_item_and_prerequisites_no_properties(ITEM_DATA_REQUIRED_VALUES_ONLY) + new_system_id = self.post_system(SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT) + + self.patch_item(item_id, {"system_id": new_system_id}) + self.check_patch_item_response_success(ITEM_GET_DATA_REQUIRED_VALUES_ONLY) + + def test_partial_update_system_id_with_non_existent_id(self): + """Test updating the `system_id` of an item to a non-existent system.""" + + item_id = self.post_item_and_prerequisites_no_properties(ITEM_DATA_REQUIRED_VALUES_ONLY) + + self.patch_item(item_id, {"system_id": str(ObjectId())}) + self.check_patch_item_failed_with_detail(422, "The specified system does not exist") + + def test_partial_update_system_id_with_invalid_id(self): + """Test updating the `system_id` of an item to an invalid ID.""" + + item_id = self.post_item_and_prerequisites_no_properties(ITEM_DATA_REQUIRED_VALUES_ONLY) + + self.patch_item(item_id, {"system_id": "invalid-id"}) + self.check_patch_item_failed_with_detail(422, "The specified system does not exist") + + def test_partial_update_usage_status_id(self): + """Test updating the `usage_status_id` of an item.""" + + item_id = self.post_item_and_prerequisites_no_properties(ITEM_DATA_REQUIRED_VALUES_ONLY) + new_usage_status_id = self.post_usage_status(USAGE_STATUS_DATA_NEW) + + self.patch_item(item_id, {"usage_status_id": new_usage_status_id}) + self.check_patch_item_response_success( + {**ITEM_GET_DATA_REQUIRED_VALUES_ONLY, "usage_status": USAGE_STATUS_DATA_NEW["value"]} + ) + + def test_partial_update_usage_status_id_with_non_existent_id(self): + """Test updating the `usage_status_id` of an item to a non-existent system.""" + + item_id = self.post_item_and_prerequisites_no_properties(ITEM_DATA_REQUIRED_VALUES_ONLY) + + self.patch_item(item_id, {"usage_status_id": str(ObjectId())}) + self.check_patch_item_failed_with_detail(422, "The specified usage status does not exist") + + def test_partial_update_usage_status_id_with_invalid_id(self): + """Test updating the `usage_status_id` of an item to an invalid ID.""" + + item_id = self.post_item_and_prerequisites_no_properties(ITEM_DATA_REQUIRED_VALUES_ONLY) + + self.patch_item(item_id, {"usage_status_id": "invalid-id"}) + self.check_patch_item_failed_with_detail(422, "The specified usage status does not exist") + + def test_partial_update_properties_with_no_properties_provided(self): + """Test updating the `properties` of an item to override none of the catalogue item properties.""" + + # All properties overridden to start with, then set to empty list to reset + item_id = self.post_item_and_prerequisites_with_properties(ITEM_DATA_WITH_ALL_PROPERTIES) + + self.patch_item(item_id, {"properties": []}) + self.check_patch_item_response_success({**ITEM_GET_DATA_WITH_ALL_PROPERTIES, "properties": []}) + + def test_partial_update_properties_with_some_properties_provided(self): + """Test updating the `properties` of an item to override some of the catalogue item properties.""" + + # No properties overridden to start with, then override some of them + item_id = self.post_item_and_prerequisites_with_properties({**ITEM_DATA_WITH_ALL_PROPERTIES, "properties": []}) + + self.patch_item(item_id, {"properties": ITEM_GET_DATA_WITH_ALL_PROPERTIES["properties"][1::]}) + self.check_patch_item_response_success( + {**ITEM_GET_DATA_WITH_ALL_PROPERTIES, "properties": ITEM_GET_DATA_WITH_ALL_PROPERTIES["properties"][1::]} + ) + + def test_partial_update_properties_with_all_properties_provided(self): + """Test updating the `properties` of an item to override all of the catalogue item properties.""" + + # No properties overridden to start with, then override all of them + item_id = self.post_item_and_prerequisites_with_properties({**ITEM_DATA_WITH_ALL_PROPERTIES, "properties": []}) + + self.patch_item(item_id, {"properties": ITEM_GET_DATA_WITH_ALL_PROPERTIES["properties"]}) + self.check_patch_item_response_success(ITEM_GET_DATA_WITH_ALL_PROPERTIES) + + def test_partial_update_properties_with_mandatory_property_given_none(self): + """Test updating the `properties` of an item to have a mandatory property with a value of `None`.""" + + item_id = self.post_item_and_prerequisites_with_properties(ITEM_DATA_WITH_ALL_PROPERTIES) + + self.patch_item(item_id, {"properties": [{**PROPERTY_DATA_BOOLEAN_MANDATORY_FALSE, "value": None}]}) + self.check_patch_item_failed_with_detail( + 422, + f"Mandatory property with ID '{self.property_name_id_dict[PROPERTY_DATA_BOOLEAN_MANDATORY_FALSE['name']]}' " + "cannot be None.", + ) + + def test_partial_update_properties_with_non_mandatory_property_given_none(self): + """Test updating the `properties` of an item to have a non-mandatory property with a value of `None`.""" + + item_id = self.post_item_and_prerequisites_with_properties(ITEM_DATA_WITH_ALL_PROPERTIES) + + self.patch_item(item_id, {"properties": [{**PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT_1, "value": None}]}) + self.check_patch_item_response_success( + { + **ITEM_GET_DATA_WITH_ALL_PROPERTIES, + "properties": [{**PROPERTY_GET_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT_1, "value": None}], + } + ) + + def test_partial_update_properties_with_string_property_with_invalid_value_type(self): + """Test updating the `properties` of an item to have an invalid value type for a string property.""" + + item_id = self.post_item_and_prerequisites_with_given_properties( + catalogue_category_properties_data=[CATALOGUE_CATEGORY_PROPERTY_DATA_STRING_MANDATORY], + catalogue_item_properties_data=[PROPERTY_DATA_STRING_MANDATORY_TEXT], + item_properties_data=[], + ) + + self.patch_item(item_id, {"properties": [{**PROPERTY_DATA_STRING_MANDATORY_TEXT, "value": 42}]}) + + self.check_patch_item_failed_with_detail( + 422, + "Invalid value type for property with ID " + f"'{self.property_name_id_dict[CATALOGUE_CATEGORY_PROPERTY_DATA_STRING_MANDATORY['name']]}'. " + "Expected type: string.", + ) + + def test_partial_update_properties_with_number_property_with_invalid_value_type(self): + """Test updating the `properties` of an item to have an invalid value type for a number property.""" + + item_id = self.post_item_and_prerequisites_with_given_properties( + catalogue_category_properties_data=[CATALOGUE_CATEGORY_PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT], + catalogue_item_properties_data=[PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT_1], + item_properties_data=[], + ) + + self.patch_item(item_id, {"properties": [{**PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT_1, "value": "42"}]}) + + self.check_patch_item_failed_with_detail( + 422, + "Invalid value type for property with ID '" + f"{self.property_name_id_dict[CATALOGUE_CATEGORY_PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT['name']]}" + "'. Expected type: number.", + ) + + def test_partial_update_properties_with_boolean_property_with_invalid_value_type(self): + """Test updating the `properties` of an item to have an invalid value type for a boolean property.""" + + item_id = self.post_item_and_prerequisites_with_given_properties( + catalogue_category_properties_data=[CATALOGUE_CATEGORY_PROPERTY_DATA_BOOLEAN_MANDATORY], + catalogue_item_properties_data=[PROPERTY_DATA_BOOLEAN_MANDATORY_FALSE], + item_properties_data=[], + ) + + self.patch_item(item_id, {"properties": [{**PROPERTY_DATA_BOOLEAN_MANDATORY_FALSE, "value": 0}]}) + + self.check_patch_item_failed_with_detail( + 422, + "Invalid value type for property with ID '" + f"{self.property_name_id_dict[CATALOGUE_CATEGORY_PROPERTY_DATA_BOOLEAN_MANDATORY['name']]}" + "'. Expected type: boolean.", + ) + + def test_partial_update_properties_with_allowed_values_list(self): + """Test updating the `properties` of an item that has allowed values lists.""" + + item_id = self.post_item_and_prerequisites_with_given_properties( + catalogue_category_properties_data=[ + CATALOGUE_CATEGORY_PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST, + CATALOGUE_CATEGORY_PROPERTY_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST, ], - ), - } - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - assert response.status_code == 200 - - item = response.json() - - assert item == { - **ITEM_POST_EXPECTED, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "usage_status": "New", - "properties": add_ids_to_properties( - catalogue_category["properties"], - [ - {"name": "Property A", "value": 20, "unit": "mm"}, - {"name": "Property B", "unit": None, "value": False}, - {"name": "Property C", "unit": "cm", "value": "25x10x5"}, - {"name": "Property D", "unit": None, "value": None}, + catalogue_item_properties_data=[ + PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_1, + PROPERTY_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE1, ], - ), - } - - -def test_partial_update_item_change_value_for_string_property_invalid_type(test_client): - """ - Test changing the value of a string item property to an invalid type. - """ - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = { - "properties": add_ids_to_properties( - catalogue_category["properties"], - [{"name": "Property C", "value": 21}], + item_properties_data=[], + ) + self.patch_item( + item_id, + { + "properties": [ + PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_2, + PROPERTY_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE2, + ] + }, ) - } - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - # pylint: disable=duplicate-code - assert response.status_code == 422 - prop_id = catalogue_category["properties"][2]["id"] - assert response.json()["detail"] == f"Invalid value type for property with ID '{prop_id}'. Expected type: string." - # pylint: enable=duplicate-code - - -def test_partial_update_item_change_value_for_number_property_invalid_type(test_client): - """ - Test changing the value of a string item property to an invalid type. - """ - # pylint: disable=duplicate-code - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = { - "properties": add_ids_to_properties( - catalogue_category["properties"], - [{"name": "Property A", "value": "21"}], + self.check_patch_item_response_success( + { + **ITEM_GET_DATA_WITH_ALL_PROPERTIES, + "properties": [ + PROPERTY_GET_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_2, + PROPERTY_GET_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE2, + ], + } ) - } - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - # pylint: disable=duplicate-code - assert response.status_code == 422 - prop_id = catalogue_category["properties"][0]["id"] - assert response.json()["detail"] == f"Invalid value type for property with ID '{prop_id}'. Expected type: number." - # pylint: enable=duplicate-code - - -def test_partial_update_item_change_value_for_boolean_property_invalid_type( - test_client, -): - """ - Test changing the value of a string item property to an invalid type. - """ - # pylint: disable=duplicate-code - catalogue_category = _post_catalogue_category_with_units(test_client, CATALOGUE_CATEGORY_POST_A) - - response = test_client.post("/v1/systems", json=SYSTEM_POST_A) - system_id = response.json()["id"] - - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) - manufacturer_id = response.json()["id"] - # pylint: enable=duplicate-code - - # pylint: disable=duplicate-code - catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, - "catalogue_category_id": catalogue_category["id"], - "manufacturer_id": manufacturer_id, - "properties": add_ids_to_properties( - catalogue_category["properties"], - CATALOGUE_ITEM_POST_A["properties"], - ), - } - response = test_client.post("/v1/catalogue-items", json=catalogue_item_post) - catalogue_item_id = response.json()["id"] - - response = test_client.post("/v1/usage-statuses", json=USAGE_STATUS_POST_A) - usage_status_id = response.json()["id"] - - # pylint: enable=duplicate-code - item_post = { - **ITEM_POST, - "catalogue_item_id": catalogue_item_id, - "system_id": system_id, - "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), - } - response = test_client.post("/v1/items", json=item_post) - - item_patch = { - "properties": add_ids_to_properties( - catalogue_category["properties"], - [{"name": "Property B", "value": 21}], + + def test_partial_update_string_property_with_allowed_values_list_to_invalid_value(self): + """Test updating the value of a string property with an allowed values list to be a value not in the list.""" + + item_id = self.post_item_and_prerequisites_with_allowed_values( + "string", {"type": "list", "values": ["value1", "value2", "value3"]}, "value1", "value2" ) - } - response = test_client.patch(f"/v1/items/{response.json()['id']}", json=item_patch) - - # pylint: disable=duplicate-code - assert response.status_code == 422 - prop_id = catalogue_category["properties"][1]["id"] - assert response.json()["detail"] == f"Invalid value type for property with ID '{prop_id}'. Expected type: boolean." - # pylint: enable=duplicate-code + + self.patch_item(item_id, {"properties": [{"name": "property", "value": "value42"}]}) + self.check_patch_item_failed_with_detail( + 422, + f"Invalid value for property with ID '{self.property_name_id_dict['property']}'. " + "Expected one of value1, value2, value3.", + ) + + def test_partial_update_string_property_with_allowed_values_list_to_invalid_value_type(self): + """Test updating the value of a string property with an allowed values list to be the wrong type.""" + + item_id = self.post_item_and_prerequisites_with_allowed_values( + "string", {"type": "list", "values": ["value1", "value2", "value3"]}, "value1", "value2" + ) + + self.patch_item(item_id, {"properties": [{"name": "property", "value": 42}]}) + self.check_patch_item_failed_with_detail( + 422, + f"Invalid value type for property with ID '{self.property_name_id_dict['property']}'. " + "Expected type: string.", + ) + + def test_partial_update_number_property_with_allowed_values_list_to_invalid_value(self): + """Test updating the value of a number property with an allowed values list to be a value not in the list.""" + + item_id = self.post_item_and_prerequisites_with_allowed_values( + "number", {"type": "list", "values": [1, 2, 3]}, 1, 2 + ) + + self.patch_item(item_id, {"properties": [{"name": "property", "value": 42}]}) + self.check_patch_item_failed_with_detail( + 422, + f"Invalid value for property with ID '{self.property_name_id_dict['property']}'. " + "Expected one of 1, 2, 3.", + ) + + def test_partial_update_number_property_with_allowed_values_list_to_invalid_value_type(self): + """Test updating the value of a number property with an allowed values list to be a value not in the list.""" + + item_id = self.post_item_and_prerequisites_with_allowed_values( + "number", {"type": "list", "values": [1, 2, 3]}, 1, 2 + ) + + self.patch_item(item_id, {"properties": [{"name": "property", "value": "2"}]}) + self.check_patch_item_failed_with_detail( + 422, + f"Invalid value type for property with ID '{self.property_name_id_dict['property']}'. " + "Expected type: number.", + ) + + def test_partial_update_with_non_existent_id(self): + """Test updating a non-existent item.""" + + self.patch_item(str(ObjectId()), {}) + self.check_patch_item_failed_with_detail(404, "Item not found") + + def test_partial_update_invalid_id(self): + """Test updating an item with an invalid ID.""" + + self.patch_item("invalid-id", {}) + self.check_patch_item_failed_with_detail(404, "Item not found") + + +class DeleteDSL(UpdateDSL): + """Base class for delete tests.""" + + _delete_response_item: Response + + def delete_item(self, item_id: str) -> None: + """ + Deletes an item with the given ID. + + :param item_id: ID of the item to be deleted. + """ + + self._delete_response_item = self.test_client.delete(f"/v1/items/{item_id}") + + def check_delete_item_success(self) -> None: + """Checks that a prior call to `delete_item` gave a successful response with the expected data + returned.""" + + assert self._delete_response_item.status_code == 204 + + def check_delete_item_failed_with_detail(self, status_code: int, detail: str) -> None: + """ + Checks that a prior call to `delete_item` gave a failed response with the expected code and + error message. + + :param status_code: Expected status code of the response. + :param detail: Expected detail given in the response. + """ + + assert self._delete_response_item.status_code == status_code + assert self._delete_response_item.json()["detail"] == detail + + +class TestDelete(DeleteDSL): + """Tests for deleting an item.""" + + def test_delete(self): + """Test deleting an item.""" + + item_id = self.post_item_and_prerequisites_no_properties(ITEM_DATA_REQUIRED_VALUES_ONLY) + + self.delete_item(item_id) + self.check_delete_item_success() + + self.get_item(item_id) + self.check_get_item_failed_with_detail(404, "An item with such ID was not found") + + def test_delete_with_non_existent_id(self): + """Test deleting a non-existent item.""" + + self.delete_item(str(ObjectId())) + self.check_delete_item_failed_with_detail(404, "Item not found") + + def test_delete_with_invalid_id(self): + """Test deleting an item with an invalid ID.""" + + self.delete_item("invalid_id") + self.check_delete_item_failed_with_detail(404, "Item not found") diff --git a/test/e2e/test_system.py b/test/e2e/test_system.py index 4c393ed1..122d7f18 100644 --- a/test/e2e/test_system.py +++ b/test/e2e/test_system.py @@ -5,13 +5,12 @@ # Expect some duplicate code inside tests as the tests for the different entities can be very similar # pylint: disable=duplicate-code -from test.conftest import add_ids_to_properties from test.e2e.conftest import E2ETestHelpers from test.e2e.mock_schemas import USAGE_STATUS_POST_B -from test.e2e.test_item import ITEM_POST from test.mock_data import ( CATALOGUE_CATEGORY_POST_DATA_LEAF_NO_PARENT_NO_PROPERTIES, CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY, + ITEM_DATA_REQUIRED_VALUES_ONLY, MANUFACTURER_POST_DATA_REQUIRED_VALUES_ONLY, SYSTEM_GET_DATA_ALL_VALUES_NO_PARENT, SYSTEM_GET_DATA_REQUIRED_VALUES_ONLY, @@ -336,7 +335,7 @@ class ListDSL(GetBreadcrumbsDSL): def get_systems(self, filters: dict) -> None: """ - Gets a list systems with the given filters. + Gets a list of systems with the given filters. :param filters: Filters to use in the request. """ @@ -631,11 +630,10 @@ def test_delete_with_child_item(self): usage_status_id = response.json()["id"] item_post = { - **ITEM_POST, + **ITEM_DATA_REQUIRED_VALUES_ONLY, "catalogue_item_id": catalogue_item_id, "system_id": system_id, "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), } self.test_client.post("/v1/items", json=item_post) # pylint: enable=duplicate-code diff --git a/test/e2e/test_usage_status.py b/test/e2e/test_usage_status.py index bbe78023..0476649a 100644 --- a/test/e2e/test_usage_status.py +++ b/test/e2e/test_usage_status.py @@ -14,7 +14,12 @@ USAGE_STATUS_POST_D, USAGE_STATUS_POST_D_EXPECTED, ) -from test.e2e.test_item import CATALOGUE_CATEGORY_POST_A, CATALOGUE_ITEM_POST_A, ITEM_POST, MANUFACTURER_POST +from test.mock_data import ( + CATALOGUE_CATEGORY_POST_DATA_LEAF_NO_PARENT_NO_PROPERTIES, + CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY, + ITEM_DATA_REQUIRED_VALUES_ONLY, + MANUFACTURER_POST_DATA_REQUIRED_VALUES_ONLY, +) from bson import ObjectId @@ -129,18 +134,20 @@ def test_delete_with_a_non_existent_id(test_client): def test_delete_usage_status_that_is_a_part_of_item(test_client): """Test trying to delete a usage status that is a part of an Item""" # pylint: disable=duplicate-code - response = test_client.post("/v1/catalogue-categories", json=CATALOGUE_CATEGORY_POST_A) + response = test_client.post( + "/v1/catalogue-categories", json=CATALOGUE_CATEGORY_POST_DATA_LEAF_NO_PARENT_NO_PROPERTIES + ) catalogue_category = response.json() response = test_client.post("/v1/systems", json=SYSTEM_POST_A) system_id = response.json()["id"] - response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST) + response = test_client.post("/v1/manufacturers", json=MANUFACTURER_POST_DATA_REQUIRED_VALUES_ONLY) manufacturer_id = response.json()["id"] catalogue_item_post = { - **CATALOGUE_ITEM_POST_A, + **CATALOGUE_ITEM_DATA_REQUIRED_VALUES_ONLY, "catalogue_category_id": catalogue_category["id"], "manufacturer_id": manufacturer_id, "properties": add_ids_to_properties( @@ -160,11 +167,10 @@ def test_delete_usage_status_that_is_a_part_of_item(test_client): usage_status_id = response.json()["id"] item_post = { - **ITEM_POST, + **ITEM_DATA_REQUIRED_VALUES_ONLY, "catalogue_item_id": catalogue_item_id, "system_id": system_id, "usage_status_id": usage_status_id, - "properties": add_ids_to_properties(catalogue_category["properties"], ITEM_POST["properties"]), } # pylint: enable=duplicate-code response = test_client.post("/v1/items", json=item_post) diff --git a/test/mock_data.py b/test/mock_data.py index 6d605e93..fc4eaa09 100644 --- a/test/mock_data.py +++ b/test/mock_data.py @@ -30,6 +30,14 @@ # --------------------------------- USAGE STATUSES --------------------------------- +# New + +USAGE_STATUS_DATA_NEW = {"value": "New"} + +USAGE_STATUS_IN_DATA_NEW = {**USAGE_STATUS_DATA_NEW, "code": "new"} + +# In Use + USAGE_STATUS_DATA_IN_USE = {"value": "In Use"} USAGE_STATUS_IN_DATA_IN_USE = {**USAGE_STATUS_DATA_IN_USE, "code": "in-use"} @@ -252,6 +260,8 @@ "value": False, } +PROPERTY_GET_DATA_BOOLEAN_MANDATORY_FALSE = {**PROPERTY_DATA_BOOLEAN_MANDATORY_FALSE} + # Boolean, Mandatory, True PROPERTY_DATA_BOOLEAN_MANDATORY_TRUE = { @@ -259,9 +269,7 @@ "value": True, } -PROPERTY_GET_DATA_BOOLEAN_MANDATORY_TRUE = { - **PROPERTY_DATA_BOOLEAN_MANDATORY_TRUE, -} +PROPERTY_GET_DATA_BOOLEAN_MANDATORY_TRUE = {**PROPERTY_DATA_BOOLEAN_MANDATORY_TRUE} # Number, Non Mandatory, Allowed Values List, 1 @@ -274,6 +282,17 @@ **PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_1 } +# Number, Non Mandatory, Allowed Values List, 2 + +PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_2 = { + "name": CATALOGUE_CATEGORY_PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST["name"], + "value": 2, +} + +PROPERTY_GET_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_2 = { + **PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_2 +} + # Number, Non Mandatory, 1 PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT_1 = { @@ -281,6 +300,8 @@ "value": 1, } +PROPERTY_GET_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT_1 = {**PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT_1} + # Number, Non Mandatory, 42 PROPERTY_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT_42 = { @@ -317,11 +338,16 @@ } # String, Non Mandatory, Allowed Values List, value2 + PROPERTY_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE2 = { **PROPERTY_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE1, "value": "value2", } +PROPERTY_GET_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE2 = { + **PROPERTY_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE2 +} + # String, Non Mandatory, Allowed Values List, None PROPERTY_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_NONE = { @@ -476,7 +502,21 @@ "usage_status_id": str(ObjectId()), } +ITEM_GET_DATA_REQUIRED_VALUES_ONLY = { + **ITEM_DATA_REQUIRED_VALUES_ONLY, + **CREATED_MODIFIED_GET_DATA_EXPECTED, + "id": ANY, + "purchase_order_number": None, + "warranty_end_date": None, + "asset_number": None, + "serial_number": None, + "delivered_date": None, + "notes": None, + "properties": [], +} + # All values, no properties + ITEM_DATA_ALL_VALUES_NO_PROPERTIES = { **ITEM_DATA_REQUIRED_VALUES_ONLY, "purchase_order_number": "1234-123", @@ -495,6 +535,13 @@ "usage_status": "In Use", } +ITEM_GET_DATA_ALL_VALUES_NO_PROPERTIES = { + **ITEM_DATA_ALL_VALUES_NO_PROPERTIES, + **CREATED_MODIFIED_GET_DATA_EXPECTED, + "id": ANY, + "properties": [], +} + # Only mandatory properties @@ -514,6 +561,15 @@ ], } +ITEM_GET_DATA_WITH_ALL_PROPERTIES = { + **ITEM_GET_DATA_REQUIRED_VALUES_ONLY, + "properties": [ + PROPERTY_GET_DATA_BOOLEAN_MANDATORY_FALSE, + PROPERTY_GET_DATA_NUMBER_NON_MANDATORY_WITH_MM_UNIT_1, + PROPERTY_GET_DATA_STRING_NON_MANDATORY_WITH_ALLOWED_VALUES_LIST_VALUE2, + ], +} + # --------------------------------- MANUFACTURERS --------------------------------- # Required values only diff --git a/test/unit/repositories/test_catalogue_category.py b/test/unit/repositories/test_catalogue_category.py index d92d1f20..a494b189 100644 --- a/test/unit/repositories/test_catalogue_category.py +++ b/test/unit/repositories/test_catalogue_category.py @@ -1,10 +1,10 @@ -# pylint: disable=too-many-lines """ Unit tests for the `CatalogueCategoryRepo` repository. """ # Expect some duplicate code inside tests as the tests for the different entities can be very similar # pylint: disable=duplicate-code +# pylint: disable=too-many-lines from test.mock_data import ( CATALOGUE_CATEGORY_IN_DATA_LEAF_NO_PARENT_NO_PROPERTIES, diff --git a/test/unit/services/test_catalogue_category.py b/test/unit/services/test_catalogue_category.py index 23014dec..04082934 100644 --- a/test/unit/services/test_catalogue_category.py +++ b/test/unit/services/test_catalogue_category.py @@ -1,9 +1,9 @@ -# pylint: disable=too-many-lines """ Unit tests for the `CatalogueCategoryService` service. """ # Expect some duplicate code inside tests as the tests for the different entities can be very similar +# pylint: disable=too-many-lines # pylint: disable=duplicate-code from test.mock_data import ( diff --git a/test/unit/services/test_catalogue_item.py b/test/unit/services/test_catalogue_item.py index 4274f769..f0b519c8 100644 --- a/test/unit/services/test_catalogue_item.py +++ b/test/unit/services/test_catalogue_item.py @@ -1,9 +1,9 @@ -# pylint: disable=too-many-lines """ Unit tests for the `CatalogueCategoryService` service. """ # Expect some duplicate code inside tests as the tests for the different entities can be very similar +# pylint: disable=too-many-lines # pylint: disable=duplicate-code from test.mock_data import ( diff --git a/test/unit/services/test_item.py b/test/unit/services/test_item.py index b7a4ba7e..c5fe40f9 100644 --- a/test/unit/services/test_item.py +++ b/test/unit/services/test_item.py @@ -1,9 +1,9 @@ -# pylint: disable=too-many-lines """ Unit tests for the `ItemService` service. """ # Expect some duplicate code inside tests as the tests for the different entities can be very similar +# pylint: disable=too-many-lines # pylint: disable=duplicate-code from test.mock_data import ( @@ -289,7 +289,7 @@ def test_create_without_properties(self): self.call_create() self.check_create_success() - def test_create_with_no_properties_defined(self): + def test_create_with_no_properties_provided(self): """Test creating an item when none of the properties present in the catalogue item are defined in the item.""" self.mock_create( @@ -301,7 +301,7 @@ def test_create_with_no_properties_defined(self): self.call_create() self.check_create_success() - def test_create_with_all_properties(self): + def test_create_with_all_properties_provided(self): """Test creating an item when all properties present in the catalogue item are defined in the item.""" self.mock_create(