diff --git a/inventory_management_system_api/models/system.py b/inventory_management_system_api/models/system.py index b1c61062..05a4b086 100644 --- a/inventory_management_system_api/models/system.py +++ b/inventory_management_system_api/models/system.py @@ -12,7 +12,7 @@ class SystemBase(BaseModel): """ - Base database model for a System + Base database model for a system """ parent_id: Optional[CustomObjectIdField] = None @@ -28,13 +28,13 @@ class SystemBase(BaseModel): class SystemIn(CreatedModifiedTimeInMixin, SystemBase): """ - Input database model for a System + Input database model for a system """ class SystemOut(CreatedModifiedTimeOutMixin, SystemBase): """ - Output database model for a System + Output database model for a system """ id: StringObjectIdField = Field(alias="_id") diff --git a/inventory_management_system_api/repositories/catalogue_category.py b/inventory_management_system_api/repositories/catalogue_category.py index 62dd2249..0a745188 100644 --- a/inventory_management_system_api/repositories/catalogue_category.py +++ b/inventory_management_system_api/repositories/catalogue_category.py @@ -72,29 +72,6 @@ def create(self, catalogue_category: CatalogueCategoryIn, session: ClientSession catalogue_category = self.get(str(result.inserted_id), session=session) return catalogue_category - def delete(self, catalogue_category_id: str, session: ClientSession = None) -> None: - """ - Delete a catalogue category by its ID from a MongoDB database. - - The method checks if the catalogue category has child elements and raises a `ChildElementsExistError` if it - does. - - :param catalogue_category_id: The ID of the catalogue category to delete. - :param session: PyMongo ClientSession to use for database operations - :raises ChildElementsExistError: If the catalogue category has child elements. - :raises MissingRecordError: If the catalogue category doesn't exist. - """ - catalogue_category_id = CustomObjectId(catalogue_category_id) - if self.has_child_elements(catalogue_category_id, session=session): - raise ChildElementsExistError( - f"Catalogue category with ID {str(catalogue_category_id)} has child elements and cannot be deleted" - ) - - logger.info("Deleting catalogue category with ID: %s from the database", catalogue_category_id) - result = self._catalogue_categories_collection.delete_one({"_id": catalogue_category_id}, session=session) - if result.deleted_count == 0: - raise MissingRecordError(f"No catalogue category found with ID: {str(catalogue_category_id)}") - def get(self, catalogue_category_id: str, session: ClientSession = None) -> Optional[CatalogueCategoryOut]: """ Retrieve a catalogue category by its ID from a MongoDB database. @@ -134,6 +111,20 @@ def get_breadcrumbs(self, catalogue_category_id: str, session: ClientSession = N collection_name="catalogue_categories", ) + def list(self, parent_id: Optional[str], session: ClientSession = None) -> List[CatalogueCategoryOut]: + """ + Retrieve catalogue categories from a MongoDB database based on the provided filters. + + :param parent_id: The parent_id to filter catalogue categories by. + :param session: PyMongo ClientSession to use for database operations + :return: A list of catalogue categories, or an empty list if no catalogue categories are returned by the + database. + """ + query = utils.list_query(parent_id, "catalogue categories") + + catalogue_categories = self._catalogue_categories_collection.find(query, session=session) + return [CatalogueCategoryOut(**catalogue_category) for catalogue_category in catalogue_categories] + def update( self, catalogue_category_id: str, catalogue_category: CatalogueCategoryIn, session: ClientSession = None ) -> CatalogueCategoryOut: @@ -192,19 +183,28 @@ def update( catalogue_category = self.get(str(catalogue_category_id), session=session) return catalogue_category - def list(self, parent_id: Optional[str], session: ClientSession = None) -> List[CatalogueCategoryOut]: + def delete(self, catalogue_category_id: str, session: ClientSession = None) -> None: """ - Retrieve catalogue categories from a MongoDB database based on the provided filters. + Delete a catalogue category by its ID from a MongoDB database. - :param parent_id: The parent_id to filter catalogue categories by. + The method checks if the catalogue category has child elements and raises a `ChildElementsExistError` if it + does. + + :param catalogue_category_id: The ID of the catalogue category to delete. :param session: PyMongo ClientSession to use for database operations - :return: A list of catalogue categories, or an empty list if no catalogue categories are returned by the - database. + :raises ChildElementsExistError: If the catalogue category has child elements. + :raises MissingRecordError: If the catalogue category doesn't exist. """ - query = utils.list_query(parent_id, "catalogue categories") + catalogue_category_id = CustomObjectId(catalogue_category_id) + if self.has_child_elements(catalogue_category_id, session=session): + raise ChildElementsExistError( + f"Catalogue category with ID {str(catalogue_category_id)} has child elements and cannot be deleted" + ) - catalogue_categories = self._catalogue_categories_collection.find(query, session=session) - return [CatalogueCategoryOut(**catalogue_category) for catalogue_category in catalogue_categories] + logger.info("Deleting catalogue category with ID: %s from the database", catalogue_category_id) + result = self._catalogue_categories_collection.delete_one({"_id": catalogue_category_id}, session=session) + if result.deleted_count == 0: + raise MissingRecordError(f"No catalogue category found with ID: {str(catalogue_category_id)}") def _is_duplicate_catalogue_category( self, diff --git a/inventory_management_system_api/repositories/system.py b/inventory_management_system_api/repositories/system.py index 9e238c8e..ebdb38f2 100644 --- a/inventory_management_system_api/repositories/system.py +++ b/inventory_management_system_api/repositories/system.py @@ -1,5 +1,5 @@ """ -Module for providing a repository for managing Systems in a MongoDB database +Module for providing a repository for managing systems in a MongoDB database """ import logging @@ -25,7 +25,7 @@ class SystemRepo: """ - Repository for managing Systems in a MongoDB database + Repository for managing systems in a MongoDB database """ def __init__(self, database: DatabaseDep) -> None: @@ -40,37 +40,37 @@ def __init__(self, database: DatabaseDep) -> None: def create(self, system: SystemIn, session: ClientSession = None) -> SystemOut: """ - Create a new System in a MongoDB database + Create a new system in a MongoDB database If a parent system is specified by `parent_id`, then checks if that exists in the database and raises a - `MissingRecordError` if it doesn't exist. It also checks if a duplicate System is found within the parent - System and raises a `DuplicateRecordError` if it is. + `MissingRecordError` if it doesn't exist. It also checks if a duplicate system is found within the parent + system and raises a `DuplicateRecordError` if it is. :param system: System to be created :param session: PyMongo ClientSession to use for database operations - :return: Created System - :raises MissingRecordError: If the parent System specified by `parent_id` doesn't exist - :raises DuplicateRecordError: If a duplicate System is found within the parent System + :return: Created system + :raises MissingRecordError: If the parent system specified by `parent_id` doesn't exist + :raises DuplicateRecordError: If a duplicate system is found within the parent system """ parent_id = str(system.parent_id) if system.parent_id else None if parent_id and not self.get(parent_id, session=session): - raise MissingRecordError(f"No parent System found with ID: {parent_id}") + raise MissingRecordError(f"No parent system found with ID: {parent_id}") if self._is_duplicate_system(parent_id, system.code, session=session): - raise DuplicateRecordError("Duplicate System found within the parent System") + raise DuplicateRecordError("Duplicate system found within the parent system") - logger.info("Inserting the new System into the database") + logger.info("Inserting the new system into the database") result = self._systems_collection.insert_one(system.model_dump(), session=session) system = self.get(str(result.inserted_id), session=session) return system def get(self, system_id: str, session: ClientSession = None) -> Optional[SystemOut]: """ - Retrieve a System by its ID from a MongoDB database + Retrieve a system by its ID from a MongoDB database - :param system_id: ID of the System to retrieve + :param system_id: ID of the system to retrieve :param session: PyMongo ClientSession to use for database operations - :return: Retrieved System or `None` if not found + :return: Retrieved system or `None` if not found """ system_id = CustomObjectId(system_id) logger.info("Retrieving system with ID: %s from the database", system_id) @@ -101,11 +101,11 @@ def get_breadcrumbs(self, system_id: str, session: ClientSession = None) -> Brea def list(self, parent_id: Optional[str], session: ClientSession = None) -> list[SystemOut]: """ - Retrieve Systems from a MongoDB database based on the provided filters + Retrieve systems from a MongoDB database based on the provided filters - :param parent_id: parent_id to filter Systems by + :param parent_id: parent_id to filter systems by :param session: PyMongo ClientSession to use for database operations - :return: List of Systems or an empty list if no Systems are retrieved + :return: List of systems or an empty list if no systems are retrieved """ query = utils.list_query(parent_id, "systems") @@ -115,26 +115,26 @@ def list(self, parent_id: Optional[str], session: ClientSession = None) -> list[ def update(self, system_id: str, system: SystemIn, session: ClientSession = None) -> SystemOut: """Update a system by its ID in a MongoDB database - :param system_id: ID of the System to update + :param system_id: ID of the system to update :param system: System containing the update data :param session: PyMongo ClientSession to use for database operations - :return: The updated System - :raises MissingRecordError: If the parent System specified by `parent_id` doesn't exist - :raises DuplicateRecordError: If a duplicate System is found within the parent System + :return: The updated system + :raises MissingRecordError: If the parent system specified by `parent_id` doesn't exist + :raises DuplicateRecordError: If a duplicate system is found within the parent system :raises InvalidActionError: If attempting to change the `parent_id` to one of its own child system ids """ system_id = CustomObjectId(system_id) parent_id = str(system.parent_id) if system.parent_id else None if parent_id and not self.get(parent_id, session=session): - raise MissingRecordError(f"No parent System found with ID: {parent_id}") + raise MissingRecordError(f"No parent system found with ID: {parent_id}") stored_system = self.get(str(system_id), session=session) moving_system = parent_id != stored_system.parent_id if (system.name != stored_system.name or moving_system) and self._is_duplicate_system( parent_id, system.code, system_id, session=session ): - raise DuplicateRecordError("Duplicate System found within the parent System") + raise DuplicateRecordError("Duplicate system found within the parent system") # Prevent a system from being moved to one of its own children if moving_system: @@ -157,14 +157,14 @@ def update(self, system_id: str, system: SystemIn, session: ClientSession = None def delete(self, system_id: str, session: ClientSession = None) -> None: """ - Delete a System by its ID from a MongoDB database + Delete a system by its ID from a MongoDB database The method checks if the system has any child and raises a `ChildElementsExistError` if it does - :param system_id: ID of the System to delete + :param system_id: ID of the system to delete :param session: PyMongo ClientSession to use for database operations - :raises ChildElementsExistError: If the System has child elements - :raises MissingRecordError: If the System doesn't exist + :raises ChildElementsExistError: If the system has child elements + :raises MissingRecordError: If the system doesn't exist """ system_id = CustomObjectId(system_id) if self._has_child_elements(system_id, session=session): @@ -173,21 +173,21 @@ def delete(self, system_id: str, session: ClientSession = None) -> None: logger.info("Deleting system with ID: %s from the database", system_id) result = self._systems_collection.delete_one({"_id": system_id}, session=session) if result.deleted_count == 0: - raise MissingRecordError(f"No System found with ID: {str(system_id)}") + raise MissingRecordError(f"No system found with ID: {str(system_id)}") def _is_duplicate_system( self, parent_id: Optional[str], code: str, system_id: CustomObjectId = None, session: ClientSession = None ) -> bool: """ - Check if a System with the same code already exists within the parent System + Check if a system with the same code already exists within the parent system - :param parent_id: ID of the parent System which can also be `None` - :param code: Code of the System to check for duplicates + :param parent_id: ID of the parent system which can also be `None` + :param code: Code of the system to check for duplicates :param system_id: The ID of the system to check if the duplicate system found is itself. :param session: PyMongo ClientSession to use for database operations - :return: `True` if a duplicate System code is found under the given parent, `False` otherwise + :return: `True` if a duplicate system code is found under the given parent, `False` otherwise """ - logger.info("Checking if System with code '%s' already exists within the parent System", code) + logger.info("Checking if system with code '%s' already exists within the parent System", code) if parent_id: parent_id = CustomObjectId(parent_id) @@ -198,11 +198,11 @@ def _is_duplicate_system( def _has_child_elements(self, system_id: CustomObjectId, session: ClientSession = None) -> bool: """ - Check if a System has any child System's or any Item's based on its ID + Check if a system has any child system's or any Item's based on its ID - :param system_id: ID of the System to check + :param system_id: ID of the system to check :param session: PyMongo ClientSession to use for database operations - :return: True if the System has child elements, False otherwise + :return: True if the system has child elements, False otherwise """ logger.info("Checking if system with ID '%s' has child elements", str(system_id)) diff --git a/inventory_management_system_api/routers/v1/system.py b/inventory_management_system_api/routers/v1/system.py index 101baf84..27eb1144 100644 --- a/inventory_management_system_api/routers/v1/system.py +++ b/inventory_management_system_api/routers/v1/system.py @@ -29,31 +29,31 @@ @router.post( path="", - summary="Create a new System", - response_description="The created System", + summary="Create a new system", + response_description="The created system", status_code=status.HTTP_201_CREATED, ) def create_system(system: SystemPostSchema, system_service: SystemServiceDep) -> SystemSchema: # pylint: disable=missing-function-docstring - logger.info("Creating a new System") + logger.info("Creating a new system") logger.debug("System data: %s", system) try: system = system_service.create(system) return SystemSchema(**system.model_dump()) except (MissingRecordError, InvalidObjectIdError) as exc: - message = "The specified parent System does not exist" + message = "The specified parent system does not exist" logger.exception(message) raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=message) from exc except DuplicateRecordError as exc: - message = "A System with the same name already exists within the same parent System" + message = "A system with the same name already exists within the same parent system" logger.exception(message) raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=message) from exc -@router.get(path="", summary="Get Systems", response_description="List of Systems") +@router.get(path="", summary="Get systems", response_description="List of systems") def get_systems( system_service: SystemServiceDep, - parent_id: Annotated[Optional[str], Query(description="Filter Systems by parent ID")] = None, + parent_id: Annotated[Optional[str], Query(description="Filter systems by parent ID")] = None, ) -> list[SystemSchema]: # pylint: disable=missing-function-docstring logger.info("Getting Systems") @@ -69,12 +69,12 @@ def get_systems( return [] -@router.get(path="/{system_id}", summary="Get a System by ID", response_description="Single System") +@router.get(path="/{system_id}", summary="Get a system by ID", response_description="Single system") def get_system( - system_id: Annotated[str, Path(description="ID of the System to get")], system_service: SystemServiceDep + system_id: Annotated[str, Path(description="ID of the system to get")], system_service: SystemServiceDep ) -> SystemSchema: # pylint: disable=missing-function-docstring - logger.info("Getting System with ID: %s", system_service) + logger.info("Getting system with ID: %s", system_service) message = "System not found" try: system = system_service.get(system_id) @@ -110,7 +110,7 @@ def get_system_breadcrumbs( # pylint: enable=duplicate-code -@router.patch(path="/{system_id}", summary="Update a System by ID", response_description="System updated successfully") +@router.patch(path="/{system_id}", summary="Update a system by ID", response_description="System updated successfully") def partial_update_system(system_id: str, system: SystemPatchSchema, system_service: SystemServiceDep) -> SystemSchema: # pylint: disable=missing-function-docstring logger.info("Partially updating system with ID: %s", system_id) @@ -121,7 +121,7 @@ def partial_update_system(system_id: str, system: SystemPatchSchema, system_serv return SystemSchema(**updated_system.model_dump()) except (MissingRecordError, InvalidObjectIdError) as exc: if system.parent_id and system.parent_id in str(exc) or "parent system" in str(exc).lower(): - message = "The specified parent System does not exist" + message = "The specified parent system does not exist" logger.exception(message) raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=message) from exc @@ -129,7 +129,7 @@ def partial_update_system(system_id: str, system: SystemPatchSchema, system_serv logger.exception(message) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=message) from exc except DuplicateRecordError as exc: - message = "A System with the same name already exists within the parent System" + message = "A system with the same name already exists within the parent system" logger.exception(message) raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=message) from exc # pylint:disable=duplicate-code diff --git a/inventory_management_system_api/schemas/system.py b/inventory_management_system_api/schemas/system.py index a465574a..33bb716d 100644 --- a/inventory_management_system_api/schemas/system.py +++ b/inventory_management_system_api/schemas/system.py @@ -12,7 +12,7 @@ class SystemImportanceType(str, Enum): """ - Enumeration for System importance types + Enumeration for system importance types """ LOW = "low" @@ -22,10 +22,10 @@ class SystemImportanceType(str, Enum): class SystemPostSchema(BaseModel): """ - Schema model for a System creation request + Schema model for a system creation request """ - parent_id: Optional[str] = Field(default=None, description="ID of the parent System (if applicable)") + parent_id: Optional[str] = Field(default=None, description="ID of the parent system (if applicable)") name: str = Field(description="Name of the system") description: Optional[str] = Field(default=None, description="Description of the system") location: Optional[str] = Field(default=None, description="Location of the system") @@ -35,7 +35,7 @@ class SystemPostSchema(BaseModel): class SystemPatchSchema(SystemPostSchema): """ - Schema model for a System update request + Schema model for a system update request """ name: Optional[str] = Field(default=None, description="Name of the system") @@ -44,8 +44,8 @@ class SystemPatchSchema(SystemPostSchema): class SystemSchema(CreatedModifiedSchemaMixin, SystemPostSchema): """ - Schema model for System get request response + Schema model for system get request response """ - id: str = Field(description="ID of the System") - code: str = Field(description="Code of the System") + id: str = Field(description="ID of the system") + code: str = Field(description="Code of the system") diff --git a/inventory_management_system_api/services/system.py b/inventory_management_system_api/services/system.py index 9c3a6e9f..c53d9c16 100644 --- a/inventory_management_system_api/services/system.py +++ b/inventory_management_system_api/services/system.py @@ -19,7 +19,7 @@ class SystemService: """ - Service for managing Systems + Service for managing systems """ def __init__(self, system_repository: Annotated[SystemRepo, Depends(SystemRepo)]) -> None: @@ -32,10 +32,10 @@ def __init__(self, system_repository: Annotated[SystemRepo, Depends(SystemRepo)] def create(self, system: SystemPostSchema) -> SystemOut: """ - Create a new System + Create a new system :param system: System to be created - :return: Created System + :return: Created system """ parent_id = system.parent_id @@ -54,10 +54,10 @@ def create(self, system: SystemPostSchema) -> SystemOut: def get(self, system_id: str) -> Optional[SystemOut]: """ - Retrieve a System by its ID + Retrieve a system by its ID - :param system_id: ID of the System to retrieve - :return: Retrieved System or `None` if not found + :param system_id: ID of the system to retrieve + :return: Retrieved system or `None` if not found """ return self._system_repository.get(system_id) @@ -72,25 +72,25 @@ def get_breadcrumbs(self, system_id: str) -> BreadcrumbsGetSchema: def list(self, parent_id: Optional[str]) -> list[SystemOut]: """ - Retrieve Systems based on the provided filters + Retrieve systems based on the provided filters - :param parent_id: parent_id to filter Systems by - :return: List of Systems or an empty list if no Systems are retrieved + :param parent_id: parent_id to filter systems by + :return: List of systems or an empty list if no systems are retrieved """ return self._system_repository.list(parent_id) def update(self, system_id: str, system: SystemPatchSchema) -> SystemOut: """ - Update a System by its ID + Update a system by its ID - :param system_id: ID of the System to updated + :param system_id: ID of the system to updated :param system: System containing the fields to be updated - :raises MissingRecordError: When the System with the given ID doesn't exist - :return: The updated System + :raises MissingRecordError: When the system with the given ID doesn't exist + :return: The updated system """ stored_system = self.get(system_id) if not stored_system: - raise MissingRecordError(f"No System found with ID: {system_id}") + raise MissingRecordError(f"No system found with ID: {system_id}") update_data = system.model_dump(exclude_unset=True) @@ -101,8 +101,8 @@ def update(self, system_id: str, system: SystemPatchSchema) -> SystemOut: def delete(self, system_id: str) -> None: """ - Delete a System by its ID + Delete a system by its ID - :param system_id: ID of the System to delete + :param system_id: ID of the system to delete """ return self._system_repository.delete(system_id) diff --git a/test/e2e/conftest.py b/test/e2e/conftest.py index e6d7cec5..21bb766b 100644 --- a/test/e2e/conftest.py +++ b/test/e2e/conftest.py @@ -2,11 +2,13 @@ Module providing test fixtures for the e2e tests. """ +from datetime import datetime from test.conftest import VALID_ACCESS_TOKEN from typing import Optional import pytest from fastapi.testclient import TestClient +from httpx import Response from inventory_management_system_api.core.database import get_database from inventory_management_system_api.main import app @@ -78,3 +80,28 @@ def replace_unit_values_with_ids_in_properties(properties_without_ids: list[dict properties.append(property_without_id) return properties + + +class E2ETestHelpers: + """ + A utility class containing common helper methods for e2e tests + + This class provides a set of static methods that encapsulate common functionality frequently used in the e2e tests + """ + + @staticmethod + def check_created_and_modified_times_updated_correctly(post_response: Response, patch_response: Response): + """Checks that an updated entity has a created_time that is the same as its original, but an updated_time + that is newer + + :param post_response: Original response for the entity post request + :param patch_response: Updated response for the entity patch request + """ + + original_data = post_response.json() + updated_data = patch_response.json() + + assert original_data["created_time"] == updated_data["created_time"] + assert datetime.fromisoformat(updated_data["modified_time"]) > datetime.fromisoformat( + original_data["modified_time"] + ) diff --git a/test/e2e/test_catalogue_category.py b/test/e2e/test_catalogue_category.py index ac73f756..d78f842e 100644 --- a/test/e2e/test_catalogue_category.py +++ b/test/e2e/test_catalogue_category.py @@ -1297,7 +1297,7 @@ def test_partial_update_catalogue_category_change_parent_id(test_client): def test_partial_update_catalogue_category_change_parent_id_to_child_id(test_client): """ - Test updating a System's parent_id to be the id of one of its children + Test updating a catalogue categories's parent_id to be the id of one of its children """ nested_categories = _post_n_catalogue_categories(test_client, 4) diff --git a/test/e2e/test_system.py b/test/e2e/test_system.py index bd8581e0..e927906f 100644 --- a/test/e2e/test_system.py +++ b/test/e2e/test_system.py @@ -1,9 +1,9 @@ """ -End-to-End tests for the System router +End-to-End tests for the system router """ from test.conftest import add_ids_to_properties -from test.e2e.conftest import replace_unit_values_with_ids_in_properties +from test.e2e.conftest import E2ETestHelpers, replace_unit_values_with_ids_in_properties from test.e2e.mock_schemas import USAGE_STATUS_POST_B from test.e2e.test_catalogue_item import CATALOGUE_CATEGORY_POST_A, CATALOGUE_ITEM_POST_A from test.e2e.test_item import ITEM_POST, MANUFACTURER_POST @@ -38,7 +38,7 @@ def setup(self, test_client): self.test_client = test_client def post_system(self, system_post_data: dict) -> Optional[str]: - """Posts a System with the given data, returns the id of the created system if successful + """Posts a system with the given data, returns the id of the created system if successful :param system_post_data: Dictionary containing the system data that should be posted :return: ID of the created system (or None if not successful) @@ -68,52 +68,52 @@ def check_post_system_failed_with_validation_message(self, status_code: int, mes class TestCreate(CreateDSL): - """Tests for creating a System""" + """Tests for creating a system""" def test_create_with_only_required_values_provided(self): - """Test creating a System with only required values provided""" + """Test creating a system with only required values provided""" self.post_system(SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY) self.check_post_system_success(SYSTEM_GET_DATA_REQUIRED_VALUES_ONLY) def test_create_with_all_values_provided(self): - """Test creating a System with all values provided""" + """Test creating a system with all values provided""" self.post_system(SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT) self.check_post_system_success(SYSTEM_GET_DATA_ALL_VALUES_NO_PARENT) def test_create_with_valid_parent_id(self): - """Test creating a System with a valid parent id""" + """Test creating a system with a valid parent id""" parent_id = self.post_system(SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT) self.post_system({**SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT, "parent_id": parent_id}) self.check_post_system_success({**SYSTEM_GET_DATA_ALL_VALUES_NO_PARENT, "parent_id": parent_id}) def test_create_with_non_existent_parent_id(self): - """Test creating a System with a non-existent parent id""" + """Test creating a system with a non-existent parent id""" self.post_system({**SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT, "parent_id": str(ObjectId())}) - self.check_post_system_failed_with_message(422, "The specified parent System does not exist") + self.check_post_system_failed_with_message(422, "The specified parent system does not exist") def test_create_with_invalid_parent_id(self): - """Test creating a System with an invalid parent id""" + """Test creating a system with an invalid parent id""" self.post_system({**SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT, "parent_id": "invalid-id"}) - self.check_post_system_failed_with_message(422, "The specified parent System does not exist") + self.check_post_system_failed_with_message(422, "The specified parent system does not exist") def test_create_with_duplicate_name_within_parent(self): - """Test creating a System with the same name as another within the same parent""" + """Test creating a system with the same name as another within the same parent""" parent_id = self.post_system(SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT) # 2nd post should be the duplicate self.post_system({**SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT, "parent_id": parent_id}) self.post_system({**SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT, "parent_id": parent_id}) self.check_post_system_failed_with_message( - 409, "A System with the same name already exists within the same parent System" + 409, "A system with the same name already exists within the same parent system" ) def test_create_with_invalid_importance(self): - """Test creating a System with an invalid importance""" + """Test creating a system with an invalid importance""" self.post_system({**SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT, "importance": "invalid-importance"}) self.check_post_system_failed_with_validation_message(422, "Input should be 'low', 'medium' or 'high'") @@ -125,7 +125,7 @@ class GetDSL(CreateDSL): _get_response: Response def get_system(self, system_id: str): - """Gets a System with the given id""" + """Gets a system with the given id""" self._get_response = self.test_client.get(f"/v1/systems/{system_id}") @@ -143,23 +143,23 @@ def check_get_system_failed_with_message(self, status_code: int, detail: str): class TestGet(GetDSL): - """Tests for getting a System""" + """Tests for getting a system""" def test_get(self): - """Test getting a System""" + """Test getting a system""" system_id = self.post_system(SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT) self.get_system(system_id) self.check_get_system_success(SYSTEM_GET_DATA_ALL_VALUES_NO_PARENT) def test_get_with_non_existent_id(self): - """Test getting a System with a non-existent id""" + """Test getting a system with a non-existent id""" self.get_system(str(ObjectId())) self.check_get_system_failed_with_message(404, "System not found") def test_get_with_invalid_id(self): - """Test getting a System with an invalid id""" + """Test getting a system with an invalid id""" self.get_system("invalid-id") self.check_get_system_failed_with_message(404, "System not found") @@ -181,8 +181,8 @@ def setup_breadcrumbs_dsl(self): def post_nested_systems(self, number: int) -> list[Optional[str]]: """Posts the given number of nested systems where each successive one has the previous as its parent - :param number: Number of Systems to create - :return: List of ids of the created Systems + :param number: Number of systems to create + :return: List of ids of the created systems """ parent_id = None @@ -196,12 +196,12 @@ def post_nested_systems(self, number: int) -> list[Optional[str]]: return [system["id"] for system in self._posted_systems_get_data] def get_system_breadcrumbs(self, system_id: str): - """Gets a System's breadcrumbs with the given id""" + """Gets a system's breadcrumbs with the given id""" self._get_response = self.test_client.get(f"/v1/systems/{system_id}/breadcrumbs") def get_last_system_breadcrumbs(self): - """Gets the last System posted's breadcrumbs""" + """Gets the last system posted's breadcrumbs""" self.get_system_breadcrumbs(self._post_response.json()["id"]) @@ -231,17 +231,17 @@ def check_get_breadcrumbs_failed_with_message(self, status_code: int, detail: st class TestGetBreadcrumbs(GetBreadcrumbsDSL): - """Tests for getting a System's breadcrumbs""" + """Tests for getting a system's breadcrumbs""" def test_get_breadcrumbs_when_no_parent(self): - """Test getting a System's breadcrumbs when the system has no parent""" + """Test getting a system's breadcrumbs when the system has no parent""" self.post_nested_systems(1) self.get_last_system_breadcrumbs() self.check_get_breadcrumbs_success(expected_trail_length=1, expected_full_trail=True) def test_get_breadcrumbs_when_trail_length_less_than_maximum(self): - """Test getting a System's breadcrumbs when the full system trail should be less than the maximum trail + """Test getting a system's breadcrumbs when the full system trail should be less than the maximum trail length""" self.post_nested_systems(BREADCRUMBS_TRAIL_MAX_LENGTH - 1) @@ -251,7 +251,7 @@ def test_get_breadcrumbs_when_trail_length_less_than_maximum(self): ) def test_get_breadcrumbs_when_trail_length_maximum(self): - """Test getting a System's breadcrumbs when the full system trail should be equal to the maximum trail + """Test getting a system's breadcrumbs when the full system trail should be equal to the maximum trail length""" self.post_nested_systems(BREADCRUMBS_TRAIL_MAX_LENGTH) @@ -259,7 +259,7 @@ def test_get_breadcrumbs_when_trail_length_maximum(self): self.check_get_breadcrumbs_success(expected_trail_length=BREADCRUMBS_TRAIL_MAX_LENGTH, expected_full_trail=True) def test_get_breadcrumbs_when_trail_length_greater_maximum(self): - """Test getting a System's breadcrumbs when the full system trail exceeds the maximum trail length""" + """Test getting a system's breadcrumbs when the full system trail exceeds the maximum trail length""" self.post_nested_systems(BREADCRUMBS_TRAIL_MAX_LENGTH + 1) self.get_last_system_breadcrumbs() @@ -268,13 +268,13 @@ def test_get_breadcrumbs_when_trail_length_greater_maximum(self): ) def test_get_breadcrumbs_with_non_existent_id(self): - """Test getting a System's breadcrumbs when given a non-existent system id""" + """Test getting a system's breadcrumbs when given a non-existent system id""" self.get_system_breadcrumbs(str(ObjectId())) self.check_get_breadcrumbs_failed_with_message(404, "System not found") def test_get_breadcrumbs_with_invalid_id(self): - """Test getting a System's breadcrumbs when given an invalid system id""" + """Test getting a system's breadcrumbs when given an invalid system id""" self.get_system_breadcrumbs("invalid_id") self.check_get_breadcrumbs_failed_with_message(404, "System not found") @@ -284,12 +284,12 @@ class ListDSL(GetBreadcrumbsDSL): """Base class for list tests""" def get_systems(self, filters: dict): - """Gets a list Systems with the given filters""" + """Gets a list systems with the given filters""" self._get_response = self.test_client.get("/v1/systems", params=filters) def post_test_system_with_child(self) -> list[dict]: - """Posts a System with a single child and returns their expected responses when returned by the list endpoint""" + """Posts a system with a single child and returns their expected responses when returned by the list endpoint""" parent_id = self.post_system(SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT) self.post_system({**SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY, "parent_id": parent_id}) @@ -310,10 +310,10 @@ def check_get_systems_failed_with_message(self, status_code: int, detail: str): class TestList(ListDSL): - """Tests for getting a list of Systems""" + """Tests for getting a list of systems""" def test_list_with_no_filters(self): - """Test getting a list of all Systems with no filters provided + """Test getting a list of all systems with no filters provided Posts a system with a child and expects both to be returned. """ @@ -323,7 +323,7 @@ def test_list_with_no_filters(self): self.check_get_systems_success(systems) def test_list_with_parent_id_filter(self): - """Test getting a list of all Systems with a parent_id filter provided + """Test getting a list of all systems with a parent_id filter provided Posts a system with a child and then filter using the parent_id expecting only the second system to be returned. @@ -334,7 +334,7 @@ def test_list_with_parent_id_filter(self): self.check_get_systems_success([systems[1]]) def test_list_with_null_parent_id_filter(self): - """Test getting a list of all Systems with a parent_id filter of "null" provided + """Test getting a list of all systems with a parent_id filter of "null" provided Posts a system with a child and then filter using a parent_id of "null" expecting only the first parent system to be returned. @@ -345,13 +345,13 @@ def test_list_with_null_parent_id_filter(self): self.check_get_systems_success([systems[0]]) def test_list_with_parent_id_filter_with_no_matching_results(self): - """Test getting a list of all Systems with a parent_id filter that returns no results""" + """Test getting a list of all systems with a parent_id filter that returns no results""" self.get_systems(filters={"parent_id": str(ObjectId())}) self.check_get_systems_success([]) def test_list_with_invalid_parent_id_filter(self): - """Test getting a list of all Systems with an invalid parent_id filter returns no results""" + """Test getting a list of all systems with an invalid parent_id filter returns no results""" self.get_systems(filters={"parent_id": "invalid-id"}) self.check_get_systems_success([]) @@ -363,7 +363,7 @@ class UpdateDSL(ListDSL): _patch_response: Response def patch_system(self, system_id: str, system_patch_data: dict): - """Updates a System with the given id""" + """Updates a system with the given id""" self._patch_response = self.test_client.patch(f"/v1/systems/{system_id}", json=system_patch_data) @@ -373,6 +373,8 @@ def check_patch_system_response_success(self, expected_system_get_data: dict): assert self._patch_response.status_code == 200 assert self._patch_response.json() == expected_system_get_data + E2ETestHelpers.check_created_and_modified_times_updated_correctly(self._post_response, self._patch_response) + def check_patch_system_failed_with_message(self, status_code: int, detail: str): """Checks that a prior call to 'patch_system' gave a failed response with the expected code and error message""" @@ -381,17 +383,17 @@ def check_patch_system_failed_with_message(self, status_code: int, detail: str): class TestUpdate(UpdateDSL): - """Tests for updating a System""" + """Tests for updating a system""" def test_partial_update_all_fields(self): - """Test updating every field of a System""" + """Test updating every field of a system""" system_id = self.post_system(SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY) self.patch_system(system_id, SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT) self.check_patch_system_response_success(SYSTEM_GET_DATA_ALL_VALUES_NO_PARENT) def test_partial_update_parent_id(self): - """Test updating the parent_id of a System""" + """Test updating the parent_id of a system""" parent_id = self.post_system(SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY) system_id = self.post_system(SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT) @@ -400,7 +402,7 @@ def test_partial_update_parent_id(self): self.check_patch_system_response_success({**SYSTEM_GET_DATA_ALL_VALUES_NO_PARENT, "parent_id": parent_id}) def test_partial_update_parent_id_to_one_with_a_duplicate_name(self): - """Test updating the parent_id of a System so that its name conflicts with one already in that other + """Test updating the parent_id of a system so that its name conflicts with one already in that other system""" # System with child @@ -411,42 +413,42 @@ def test_partial_update_parent_id_to_one_with_a_duplicate_name(self): self.patch_system(system_id, {"parent_id": parent_id}) self.check_patch_system_failed_with_message( - 409, "A System with the same name already exists within the parent System" + 409, "A system with the same name already exists within the parent system" ) def test_partial_update_parent_id_to_child_of_self(self): - """Test updating the parent_id of a System to one of its own children""" + """Test updating the parent_id of a system to one of its own children""" system_ids = self.post_nested_systems(2) self.patch_system(system_ids[0], {"parent_id": system_ids[1]}) self.check_patch_system_failed_with_message(422, "Cannot move a system to one of its own children") def test_partial_update_parent_id_to_non_existent(self): - """Test updating the parent_id of a System to a non-existent System""" + """Test updating the parent_id of a system to a non-existent system""" system_id = self.post_system(SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT) self.patch_system(system_id, {"parent_id": str(ObjectId())}) - self.check_patch_system_failed_with_message(422, "The specified parent System does not exist") + self.check_patch_system_failed_with_message(422, "The specified parent system does not exist") def test_partial_update_parent_id_to_invalid(self): - """Test updating the parent_id of a System to an invalid id""" + """Test updating the parent_id of a system to an invalid id""" system_id = self.post_system(SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT) self.patch_system(system_id, {"parent_id": "invalid-id"}) - self.check_patch_system_failed_with_message(422, "The specified parent System does not exist") + self.check_patch_system_failed_with_message(422, "The specified parent system does not exist") def test_partial_update_name_to_duplicate(self): - """Test updating the name of a System to conflict with a pre-existing one""" + """Test updating the name of a system to conflict with a pre-existing one""" self.post_system(SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY) system_id = self.post_system(SYSTEM_POST_DATA_ALL_VALUES_NO_PARENT) self.patch_system(system_id, {"name": SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY["name"]}) self.check_patch_system_failed_with_message( - 409, "A System with the same name already exists within the parent System" + 409, "A system with the same name already exists within the parent system" ) def test_partial_update_name_capitalisation(self): - """Test updating the capitalisation of the name of a System (to ensure it the check doesn't confuse with + """Test updating the capitalisation of the name of a system (to ensure it the check doesn't confuse with duplicates)""" system_id = self.post_system({**SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY, "name": "Test system"}) @@ -456,13 +458,13 @@ def test_partial_update_name_capitalisation(self): ) def test_partial_update_with_non_existent_id(self): - """Test updating a non-existent System""" + """Test updating a non-existent system""" self.patch_system(str(ObjectId()), {}) self.check_patch_system_failed_with_message(404, "System not found") def test_partial_update_invalid_id(self): - """Test updating a System with an invalid id""" + """Test updating a system with an invalid id""" self.patch_system("invalid-id", {}) self.check_patch_system_failed_with_message(404, "System not found") @@ -474,7 +476,7 @@ class DeleteDSL(UpdateDSL): _delete_response: Response def delete_system(self, system_id: str): - """Deletes a System with the given id""" + """Deletes a system with the given id""" self._delete_response = self.test_client.delete(f"/v1/systems/{system_id}") @@ -492,10 +494,10 @@ def check_delete_failed_with_message(self, status_code: int, detail: str): class TestDelete(DeleteDSL): - """Tests for deleting a System""" + """Tests for deleting a system""" def test_delete(self): - """Test deleting a System""" + """Test deleting a system""" system_id = self.post_system(SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY) self.delete_system(system_id) @@ -505,18 +507,17 @@ def test_delete(self): self.check_get_system_failed_with_message(404, "System not found") def test_delete_with_child_system(self): - """Test deleting a System with a child system""" + """Test deleting a system with a child system""" system_ids = self.post_nested_systems(2) self.delete_system(system_ids[0]) self.check_delete_failed_with_message(409, "System has child elements and cannot be deleted") def test_delete_with_child_item(self): - """Test deleting a System with a child system""" + """Test deleting a system with a child system""" # pylint:disable=fixme # TODO: THIS SHOULD BE CLEANED UP IN FUTURE - system_id = self.post_system(SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY) self.post_system({**SYSTEM_POST_DATA_REQUIRED_VALUES_ONLY, "parent_id": system_id}) @@ -570,13 +571,13 @@ def test_delete_with_child_item(self): self.check_delete_failed_with_message(409, "System has child elements and cannot be deleted") def test_delete_with_non_existent_id(self): - """Test deleting a non-existent System""" + """Test deleting a non-existent system""" self.delete_system(str(ObjectId())) self.check_delete_failed_with_message(404, "System not found") def test_delete_with_invalid_id(self): - """Test deleting a System with an invalid id""" + """Test deleting a system with an invalid id""" self.delete_system("invalid_id") self.check_delete_failed_with_message(404, "System not found") diff --git a/test/e2e/test_unit.py b/test/e2e/test_unit.py index ab33dfbd..c3e41335 100644 --- a/test/e2e/test_unit.py +++ b/test/e2e/test_unit.py @@ -156,7 +156,7 @@ def test_delete_with_a_non_existent_id(test_client): def test_delete_unit_that_is_a_part_of_catalogue_category(test_client): - """Test trying to delete a unit that is a part of a Catalogue Category""" + """Test trying to delete a unit that is a part of a catalogue category""" response = test_client.post("/v1/units", json=UNIT_POST_A) unit_mm = response.json() diff --git a/test/mock_data.py b/test/mock_data.py index 89ec1f31..6f8df591 100644 --- a/test/mock_data.py +++ b/test/mock_data.py @@ -12,10 +12,62 @@ from unittest.mock import ANY +from inventory_management_system_api.models.catalogue_category import CatalogueCategoryPropertyIn + # Used for _GET_DATA's as when comparing these will not be possible to know # at runtime CREATED_MODIFIED_GET_DATA_EXPECTED = {"created_time": ANY, "modified_time": ANY} +# --------------------------------- CATALOGUE CATEGORIES --------------------------------- + +CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A = { + "name": "Category A", + "code": "category-a", + "is_leaf": False, + "parent_id": None, + "properties": [], +} + +CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_B = { + "name": "Category B", + "code": "catagory-b", + "is_leaf": False, + "parent_id": None, + "properties": [], +} + +CATALOGUE_CATEGORY_IN_DATA_LEAF_NO_PARENT_NO_PROPERTIES = { + "name": "Leaf Category No Parent No Properties", + "code": "leaf-category-no-parent-no-properties", + "is_leaf": True, + "parent_id": None, + "properties": [], +} + +CATALOGUE_CATEGORY_PROPERTY_BOOLEAN_MANDATORY_WITHOUT_UNIT = { + "name": "Mandatory Boolean Property Without Unit", + "type": "boolean", + "mandatory": True, +} + +CATALOGUE_CATEGORY_PROPERTY_NUMBER_NON_MANDATORY_WITH_UNIT = { + "name": "Non Mandatory Number Property With Unit", + "type": "number", + "unit": "mm", + "mandatory": False, +} + +CATALOGUE_CATEGORY_IN_DATA_LEAF_NO_PARENT_WITH_PROPERTIES = { + "name": "Leaf Category No Parent With Properties", + "code": "leaf-category-no-parent-with-properties", + "is_leaf": True, + "parent_id": None, + "properties": [ + CatalogueCategoryPropertyIn(**CATALOGUE_CATEGORY_PROPERTY_BOOLEAN_MANDATORY_WITHOUT_UNIT), + CatalogueCategoryPropertyIn(**CATALOGUE_CATEGORY_PROPERTY_NUMBER_NON_MANDATORY_WITH_UNIT), + ], +} + # --------------------------------- MANUFACTURERS --------------------------------- # Required values only diff --git a/test/unit/repositories/conftest.py b/test/unit/repositories/conftest.py index 8a582f3e..262f78e3 100644 --- a/test/unit/repositories/conftest.py +++ b/test/unit/repositories/conftest.py @@ -12,7 +12,6 @@ from pymongo.database import Database from pymongo.results import DeleteResult, InsertOneResult, UpdateResult -from inventory_management_system_api.repositories.catalogue_category import CatalogueCategoryRepo from inventory_management_system_api.repositories.catalogue_item import CatalogueItemRepo from inventory_management_system_api.repositories.item import ItemRepo from inventory_management_system_api.repositories.manufacturer import ManufacturerRepo @@ -38,17 +37,6 @@ def fixture_database_mock() -> Mock: return database_mock -@pytest.fixture(name="catalogue_category_repository") -def fixture_catalogue_category_repository(database_mock: Mock) -> CatalogueCategoryRepo: - """ - Fixture to create a `CatalogueCategoryRepo` instance with a mocked Database dependency. - - :param database_mock: Mocked MongoDB database instance. - :return: `CatalogueCategoryRepo` instance with the mocked dependency. - """ - return CatalogueCategoryRepo(database_mock) - - @pytest.fixture(name="catalogue_item_repository") def fixture_catalogue_item_repository(database_mock: Mock) -> CatalogueItemRepo: """ @@ -189,6 +177,8 @@ def mock_update_many(collection_mock: Mock) -> None: collection_mock.update_many.return_value = update_many_result_mock +# pylint:disable=fixme +# TODO: Remove this once tests refactored - should be able to just use `RepositoryTestHelpers.` @pytest.fixture(name="test_helpers") def fixture_test_helpers() -> Type[RepositoryTestHelpers]: """ diff --git a/test/unit/repositories/mock_models.py b/test/unit/repositories/mock_models.py index f3e32da0..a3d1ebb3 100644 --- a/test/unit/repositories/mock_models.py +++ b/test/unit/repositories/mock_models.py @@ -11,14 +11,6 @@ "modified_time": datetime(2024, 2, 16, 14, 1, 13, 0, tzinfo=timezone.utc), } -MOCK_CATALOGUE_CATEGORY_PROPERTY_A_INFO = { - "id": str(ObjectId()), - "name": "Property A", - "type": "number", - "unit": "mm", - "mandatory": False, -} - MOCK_PROPERTY_A_INFO = { "id": str(ObjectId()), "name": "Property A", diff --git a/test/unit/repositories/test_catalogue_category.py b/test/unit/repositories/test_catalogue_category.py index 4de17ae2..5a9185d7 100644 --- a/test/unit/repositories/test_catalogue_category.py +++ b/test/unit/repositories/test_catalogue_category.py @@ -2,13 +2,23 @@ """ Unit tests for the `CatalogueCategoryRepo` repository. """ -from test.unit.repositories.mock_models import MOCK_CATALOGUE_CATEGORY_PROPERTY_A_INFO, MOCK_CREATED_MODIFIED_TIME -from test.unit.repositories.test_catalogue_item import FULL_CATALOGUE_ITEM_A_INFO + +from test.mock_data import ( + CATALOGUE_CATEGORY_IN_DATA_LEAF_NO_PARENT_NO_PROPERTIES, + CATALOGUE_CATEGORY_IN_DATA_LEAF_NO_PARENT_WITH_PROPERTIES, + CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, + CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_B, + CATALOGUE_CATEGORY_PROPERTY_NUMBER_NON_MANDATORY_WITH_UNIT, +) +from test.unit.repositories.conftest import RepositoryTestHelpers from test.unit.repositories.test_utils import ( MOCK_BREADCRUMBS_QUERY_RESULT_LESS_THAN_MAX_LENGTH, + MOCK_MOVE_QUERY_RESULT_INVALID, MOCK_MOVE_QUERY_RESULT_VALID, ) -from unittest.mock import MagicMock, call, patch +from test.unit.services.test_catalogue_item import CATALOGUE_ITEM_A_INFO +from typing import Optional +from unittest.mock import MagicMock, Mock, call, patch import pytest from bson import ObjectId @@ -27,1334 +37,1204 @@ CatalogueCategoryPropertyIn, CatalogueCategoryPropertyOut, ) +from inventory_management_system_api.repositories.catalogue_category import CatalogueCategoryRepo -CATALOGUE_CATEGORY_INFO = { - "name": "Category A", - "code": "category-a", - "is_leaf": False, - "parent_id": None, - "properties": [], -} +class CatalogueCategoryRepoDSL: + """Base class for CatalogueCategoryRepo unit tests""" -def test_create(test_helpers, database_mock, catalogue_category_repository): - """ - Test creating a catalogue category. + # pylint:disable=too-many-instance-attributes + mock_database: Mock + mock_utils: Mock + catalogue_category_repository: CatalogueCategoryRepo + catalogue_categories_collection: Mock + catalogue_items_collection: Mock - Verify that the `create` method properly handles the catalogue category to be created, checks that there is not a - duplicate catalogue category, and creates the catalogue category. - """ - # pylint: disable=duplicate-code - catalogue_category_in = CatalogueCategoryIn( - name="Category A", - code="category-a", - is_leaf=False, - parent_id=None, - properties=[], - ) - catalogue_category_info = catalogue_category_in.model_dump(by_alias=True) - catalogue_category_out = CatalogueCategoryOut(id=str(ObjectId()), **catalogue_category_info) - session = MagicMock() - # pylint: enable=duplicate-code + mock_session = MagicMock() - # Mock `find_one` to return no duplicate catalogue categories found - test_helpers.mock_find_one(database_mock.catalogue_categories, None) + # Internal data for utility functions + _mock_child_catalogue_category_data: Optional[dict] + _mock_child_catalogue_item_data: Optional[dict] - # Mock `insert_one` to return an object for the inserted catalogue category document - test_helpers.mock_insert_one(database_mock.catalogue_categories, CustomObjectId(catalogue_category_out.id)) - # Mock `find_one` to return the inserted catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **catalogue_category_info, - "_id": CustomObjectId(catalogue_category_out.id), - }, - ) + @pytest.fixture(autouse=True) + def setup(self, database_mock): + """Setup fixtures""" - created_catalogue_category = catalogue_category_repository.create(catalogue_category_in, session=session) + self.mock_database = database_mock + self.catalogue_category_repository = CatalogueCategoryRepo(database_mock) + self.catalogue_categories_collection = database_mock.catalogue_categories + self.catalogue_items_collection = database_mock.catalogue_items - database_mock.catalogue_categories.insert_one.assert_called_once_with(catalogue_category_info, session=session) - assert created_catalogue_category == catalogue_category_out + self.mock_session = MagicMock() + # Here we only wrap the utils so they keep their original functionality - this is done here + # to avoid having to mock the code generation function as the output will be passed to + # CatalogueCategoryOut with pydantic validation and so will error otherwise + with patch("inventory_management_system_api.repositories.catalogue_category.utils") as mock_utils: + self.mock_utils = mock_utils + yield -def test_create_leaf_category_without_properties(test_helpers, database_mock, catalogue_category_repository): - """ - Test creating a leaf catalogue category without properties. + def mock_has_child_elements( + self, child_catalogue_category_data: Optional[dict] = None, child_catalogue_item_data: Optional[dict] = None + ): + """Mocks database methods appropriately for when the 'has_child_elements' repo method will be called - Verify that the `create` method properly handles the catalogue category to be created, checks that there is not a - duplicate catalogue category, and creates the catalogue category. - """ - # pylint: disable=duplicate-code - catalogue_category_in = CatalogueCategoryIn( - name="Category A", - code="category-a", - is_leaf=True, - parent_id=None, - properties=[], - ) - catalogue_category_info = catalogue_category_in.model_dump(by_alias=True) - catalogue_category_out = CatalogueCategoryOut(id=str(ObjectId()), **catalogue_category_info) - session = MagicMock() - # pylint: enable=duplicate-code + :param child_catalogue_category_data: Dictionary containing a child catalogue category's data (or None) + :param child_catalogue_item_data: Dictionary containing a child catalogue item's data (or None) + """ - # Mock `find_one` to return no duplicate catalogue categories found - test_helpers.mock_find_one(database_mock.catalogue_categories, None) - # Mock `insert_one` to return an object for the inserted catalogue category document - test_helpers.mock_insert_one(database_mock.catalogue_categories, CustomObjectId(catalogue_category_out.id)) - # Mock `find_one` to return the inserted catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - {**catalogue_category_info, "_id": CustomObjectId(catalogue_category_out.id)}, - ) + self._mock_child_catalogue_category_data = child_catalogue_category_data + self._mock_child_catalogue_item_data = child_catalogue_item_data - created_catalogue_category = catalogue_category_repository.create(catalogue_category_in, session=session) + RepositoryTestHelpers.mock_find_one(self.catalogue_categories_collection, child_catalogue_category_data) + RepositoryTestHelpers.mock_find_one(self.catalogue_items_collection, child_catalogue_item_data) - database_mock.catalogue_categories.insert_one.assert_called_once_with(catalogue_category_info, session=session) - assert created_catalogue_category == catalogue_category_out + def check_has_child_elements_performed_expected_calls(self, expected_catalogue_category_id: str): + """Checks that a call to `has_child_elements` performed the expected function calls + :param expected_catalogue_category_id: Expected catalogue category id used in the database calls + """ -def test_create_leaf_category_with_properties(test_helpers, database_mock, catalogue_category_repository): - """ - Test creating a leaf catalogue category with properties. + self.catalogue_categories_collection.find_one.assert_called_once_with( + {"parent_id": CustomObjectId(expected_catalogue_category_id)}, session=self.mock_session + ) + # Will only call the second one if the first doesn't return anything + if not self._mock_child_catalogue_category_data: + self.catalogue_items_collection.find_one.assert_called_once_with( + {"catalogue_category_id": CustomObjectId(expected_catalogue_category_id)}, session=self.mock_session + ) + + +class CreateDSL(CatalogueCategoryRepoDSL): + """Base class for create tests""" + + _catalogue_category_in: CatalogueCategoryIn + _expected_catalogue_category_out: CatalogueCategoryOut + _created_catalogue_category: CatalogueCategoryOut + _create_exception: pytest.ExceptionInfo + + def mock_create( + self, + catalogue_category_in_data: dict, + parent_catalogue_category_in_data: Optional[dict] = None, + duplicate_catalogue_category_in_data: Optional[dict] = None, + ): + """Mocks database methods appropriately to test the 'create' repo method + + :param catalogue_category_in_data: Dictionary containing the catalogue category data as would be required for + a CatalogueCategoryIn database model (i.e. no id or created and modified + times required) + :param parent_catalogue_category_in_data: Either None or a dictionary containing the parent catalogue category + data as would be required for a CatalogueCategoryIn database model + :param duplicate_catalogue_category_in_data: Either None or a dictionary containing catalogue category data + for a duplicate catalogue category + """ + inserted_catalogue_category_id = CustomObjectId(str(ObjectId())) + + # Pass through CatalogueCategoryIn first as need creation and modified times + self._catalogue_category_in = CatalogueCategoryIn(**catalogue_category_in_data) + + self._expected_catalogue_category_out = CatalogueCategoryOut( + **self._catalogue_category_in.model_dump(by_alias=True), id=inserted_catalogue_category_id + ) - Verify that the `create` method properly handles the catalogue category to be created, checks that there is not a - duplicate catalogue category, and creates the catalogue category. - """ - # pylint: disable=duplicate-code - catalogue_category_in = CatalogueCategoryIn( - name="Category A", - code="category-a", - is_leaf=True, - parent_id=None, - properties=[ - CatalogueCategoryPropertyIn(name="Property A", type="number", unit="mm", mandatory=False), - CatalogueCategoryPropertyIn(name="Property B", type="boolean", mandatory=True), - ], - ) - catalogue_category_info = catalogue_category_in.model_dump(by_alias=True) - catalogue_category_out = CatalogueCategoryOut(id=str(ObjectId()), **catalogue_category_info) - session = MagicMock() - # pylint: enable=duplicate-code + # When a parent_id is given, need to mock the find_one for it too + if catalogue_category_in_data["parent_id"]: + # If parent_catalogue_category_data is given as None, then it is intentionally supposed to be, otherwise + # pass through CatalogueCategoryIn first to ensure it has creation and modified times + RepositoryTestHelpers.mock_find_one( + self.catalogue_categories_collection, + ( + { + **CatalogueCategoryIn(**parent_catalogue_category_in_data).model_dump(), + "_id": catalogue_category_in_data["parent_id"], + } + if parent_catalogue_category_in_data + else None + ), + ) + RepositoryTestHelpers.mock_find_one( + self.catalogue_categories_collection, + ( + {**CatalogueCategoryIn(**duplicate_catalogue_category_in_data).model_dump(), "_id": ObjectId()} + if duplicate_catalogue_category_in_data + else None + ), + ) + RepositoryTestHelpers.mock_insert_one(self.catalogue_categories_collection, inserted_catalogue_category_id) + RepositoryTestHelpers.mock_find_one( + self.catalogue_categories_collection, + {**self._catalogue_category_in.model_dump(by_alias=True), "_id": inserted_catalogue_category_id}, + ) - # Mock `find_one` to return no duplicate catalogue categories found - test_helpers.mock_find_one(database_mock.catalogue_categories, None) - # Mock `insert_one` to return an object for the inserted catalogue category document - test_helpers.mock_insert_one(database_mock.catalogue_categories, CustomObjectId(catalogue_category_out.id)) - # Mock `find_one` to return the inserted catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **catalogue_category_info, - "_id": CustomObjectId(catalogue_category_out.id), - }, - ) + def call_create(self): + """Calls the CatalogueCategoryRepo `create` method with the appropriate data from a prior call to + `mock_create`""" - created_catalogue_category = catalogue_category_repository.create(catalogue_category_in, session=session) + self._created_catalogue_category = self.catalogue_category_repository.create( + self._catalogue_category_in, session=self.mock_session + ) - database_mock.catalogue_categories.insert_one.assert_called_once_with(catalogue_category_info, session=session) - assert created_catalogue_category == catalogue_category_out + def call_create_expecting_error(self, error_type: type[BaseException]): + """Calls the CatalogueCategoryRepo `create` method with the appropriate data from a prior call to `mock_create` + while expecting an error to be raised""" + with pytest.raises(error_type) as exc: + self.catalogue_category_repository.create(self._catalogue_category_in) + self._create_exception = exc -def test_create_with_parent_id(test_helpers, database_mock, catalogue_category_repository): - """ - Test creating a catalogue category with a parent ID. + def check_create_success(self): + """Checks that a prior call to `call_create` worked as expected""" - Verify that the `create` method properly handles a catalogue category with a parent ID. - """ - # pylint: disable=duplicate-code - catalogue_category_in = CatalogueCategoryIn( - name="Category B", - code="category-b", - is_leaf=True, - parent_id=str(ObjectId()), - properties=[ - CatalogueCategoryPropertyIn(name="Property A", type="number", unit="mm", mandatory=False), - CatalogueCategoryPropertyIn(name="Property B", type="boolean", mandatory=True), - ], - ) - catalogue_category_info = catalogue_category_in.model_dump(by_alias=True) - catalogue_category_out = CatalogueCategoryOut(id=str(ObjectId()), **catalogue_category_info) - session = MagicMock() - # pylint: enable=duplicate-code + catalogue_category_in_data = self._catalogue_category_in.model_dump(by_alias=True) - # Mock `find_one` to return the parent catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **catalogue_category_info, - "_id": CustomObjectId(catalogue_category_out.parent_id), - }, - ) - - # Mock `find_one` to return no duplicate catalogue categories found - test_helpers.mock_find_one(database_mock.catalogue_categories, None) - - # Mock `insert_one` to return an object for the inserted catalogue category document - test_helpers.mock_insert_one(database_mock.catalogue_categories, CustomObjectId(catalogue_category_out.id)) - - # Mock `find_one` to return the inserted catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **catalogue_category_info, - "_id": CustomObjectId(catalogue_category_out.id), - }, - ) - - created_catalogue_category = catalogue_category_repository.create(catalogue_category_in, session=session) - - database_mock.catalogue_categories.insert_one.assert_called_once_with(catalogue_category_info, session=session) - database_mock.catalogue_categories.find_one.assert_has_calls( - [ - call({"_id": CustomObjectId(catalogue_category_out.parent_id)}, session=session), + # Obtain a list of expected find_one calls + expected_find_one_calls = [] + # This is the check for parent existence + if self._catalogue_category_in.parent_id: + expected_find_one_calls.append( + call({"_id": self._catalogue_category_in.parent_id}, session=self.mock_session) + ) + # Also need checks for duplicate and the final newly inserted catalogue category get + expected_find_one_calls.append( call( { - "parent_id": CustomObjectId(catalogue_category_out.parent_id), - "code": catalogue_category_out.code, + "parent_id": self._catalogue_category_in.parent_id, + "code": self._catalogue_category_in.code, "_id": {"$ne": None}, }, - session=session, - ), - call({"_id": CustomObjectId(catalogue_category_out.id)}, session=session), - ] - ) - assert created_catalogue_category == catalogue_category_out + session=self.mock_session, + ) + ) + expected_find_one_calls.append( + call( + {"_id": CustomObjectId(self._expected_catalogue_category_out.id)}, + session=self.mock_session, + ) + ) + self.catalogue_categories_collection.find_one.assert_has_calls(expected_find_one_calls) + self.catalogue_categories_collection.insert_one.assert_called_once_with( + catalogue_category_in_data, session=self.mock_session + ) + assert self._created_catalogue_category == self._expected_catalogue_category_out -def test_create_with_non_existent_parent_id(test_helpers, database_mock, catalogue_category_repository): - """ - Test creating a catalogue category with a non-existent parent ID. + def check_create_failed_with_exception(self, message: str): + """Checks that a prior call to `call_create_expecting_error` worked as expected, raising an exception + with the correct message""" - Verify that the `create` method properly handles a catalogue category with a non-existent parent ID, does not find a - parent catalogue category with an ID specified by `parent_id`, and does not create the catalogue category. - """ - # pylint: disable=duplicate-code - catalogue_category_in = CatalogueCategoryIn( - name="Category A", - code="category-a", - is_leaf=False, - parent_id=str(ObjectId()), - properties=[], - ) - catalogue_category_info = catalogue_category_in.model_dump(by_alias=True) - catalogue_category_out = CatalogueCategoryOut(id=str(ObjectId()), **catalogue_category_info) - # pylint: enable=duplicate-code + self.catalogue_categories_collection.insert_one.assert_not_called() - # Mock `find_one` to not return a parent catalogue category document - test_helpers.mock_find_one(database_mock.catalogue_categories, None) + assert str(self._create_exception.value) == message - with pytest.raises(MissingRecordError) as exc: - catalogue_category_repository.create(catalogue_category_in) - database_mock.catalogue_categories.insert_one.assert_not_called() - assert str(exc.value) == f"No parent catalogue category found with ID: {catalogue_category_out.parent_id}" +class TestCreate(CreateDSL): + """Tests for creating a catalogue category""" + def test_create_non_leaf_without_parent(self): + """Test creating a non-leaf catalogue category without a parent""" -def test_create_with_duplicate_name_within_parent(test_helpers, database_mock, catalogue_category_repository): - """ - Test creating a catalogue category with a duplicate name within the parent catalogue category. + self.mock_create(CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A) + self.call_create() + self.check_create_success() - Verify that the `create` method properly handles a catalogue category with a duplicate name, finds that there is a - duplicate catalogue category, and does not create the catalogue category. - """ - # pylint: disable=duplicate-code - catalogue_category_in = CatalogueCategoryIn( - name="Category B", - code="category-b", - is_leaf=True, - parent_id=str(ObjectId()), - properties=[ - CatalogueCategoryPropertyIn(name="Property A", type="number", unit="mm", mandatory=False), - CatalogueCategoryPropertyIn(name="Property B", type="boolean", mandatory=True), - ], - ) - catalogue_category_info = catalogue_category_in.model_dump(by_alias=True) - catalogue_category_out = CatalogueCategoryOut(id=str(ObjectId()), **catalogue_category_info) - # pylint: enable=duplicate-code + def test_create_leaf_without_properties(self): + """Test creating a leaf catalogue category without properties""" - # Mock `find_one` to return the parent catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **catalogue_category_info, - "_id": CustomObjectId(catalogue_category_out.parent_id), - }, - ) - # Mock `find_one` to return duplicate catalogue category found - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **catalogue_category_info, - "_id": ObjectId(), - "parent_id": CustomObjectId(catalogue_category_out.parent_id), - }, - ) - - with pytest.raises(DuplicateRecordError) as exc: - catalogue_category_repository.create(catalogue_category_in) - - assert str(exc.value) == "Duplicate catalogue category found within the parent catalogue category" - database_mock.catalogue_categories.find_one.assert_called_with( - { - "parent_id": CustomObjectId(catalogue_category_out.parent_id), - "code": catalogue_category_out.code, - "_id": {"$ne": None}, - }, - session=None, - ) - - -def test_delete(test_helpers, database_mock, catalogue_category_repository): - """ - Test deleting a catalogue category. - - Verify that the `delete` method properly handles the deletion of a catalogue category by ID. - """ - catalogue_category_id = str(ObjectId()) - session = MagicMock() - - # Mock `delete_one` to return that one document has been deleted - test_helpers.mock_delete_one(database_mock.catalogue_categories, 1) - - # Mock `find_one` to return no child catalogue category document - test_helpers.mock_find_one(database_mock.catalogue_items, None) - test_helpers.mock_find_one(database_mock.catalogue_categories, None) - - catalogue_category_repository.delete(catalogue_category_id, session=session) - - database_mock.catalogue_categories.delete_one.assert_called_once_with( - {"_id": CustomObjectId(catalogue_category_id)}, session=session - ) - - -def test_delete_with_child_catalogue_categories(test_helpers, database_mock, catalogue_category_repository): - """ - Test deleting a catalogue category with child catalogue categories. - - Verify that the `delete` method properly handles the deletion of a catalogue category with child catalogue - categories. - """ - catalogue_category_id = str(ObjectId()) - - # Mock find_one to return children catalogue category found - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **CATALOGUE_CATEGORY_INFO, - "_id": CustomObjectId(str(ObjectId())), - "parent_id": catalogue_category_id, - }, - ) - # Mock find_one to return no children catalogue items found - test_helpers.mock_find_one(database_mock.catalogue_items, None) - - with pytest.raises(ChildElementsExistError) as exc: - catalogue_category_repository.delete(catalogue_category_id) - assert str(exc.value) == ( - f"Catalogue category with ID {catalogue_category_id} has child elements and cannot be deleted" - ) - - -def test_delete_with_child_catalogue_items(test_helpers, database_mock, catalogue_category_repository): - """ - Test deleting a catalogue category with child catalogue items. - - Verify that the `delete` method properly handles the deletion of a catalogue category with child catalogue items. - """ - catalogue_category_id = str(ObjectId()) - - # Mock `find_one` to return no child catalogue category document - test_helpers.mock_find_one(database_mock.catalogue_categories, None) - # pylint: disable=duplicate-code - # Mock `find_one` to return the child catalogue item document - test_helpers.mock_find_one( - database_mock.catalogue_items, - { - **FULL_CATALOGUE_ITEM_A_INFO, - "_id": CustomObjectId(str(ObjectId())), - "catalogue_category_id": CustomObjectId(catalogue_category_id), - }, - ) - # pylint: enable=duplicate-code - with pytest.raises(ChildElementsExistError) as exc: - catalogue_category_repository.delete(catalogue_category_id) - assert str(exc.value) == ( - f"Catalogue category with ID {catalogue_category_id} has child elements and cannot be deleted" - ) + self.mock_create(CATALOGUE_CATEGORY_IN_DATA_LEAF_NO_PARENT_NO_PROPERTIES) + self.call_create() + self.check_create_success() + def test_create_leaf_with_properties(self): + """Test creating a leaf catalogue category with properties""" -def test_delete_with_invalid_id(catalogue_category_repository): - """ - Test deleting a catalogue category with an invalid ID. + self.mock_create(CATALOGUE_CATEGORY_IN_DATA_LEAF_NO_PARENT_WITH_PROPERTIES) + self.call_create() + self.check_create_success() - Verify that the `delete` method properly handles the deletion of a catalogue category with an invalid ID. - """ - with pytest.raises(InvalidObjectIdError) as exc: - catalogue_category_repository.delete("invalid") - assert str(exc.value) == "Invalid ObjectId value 'invalid'" + def test_create_with_parent(self): + """Test creating a catalogue category with a parent""" + self.mock_create( + {**CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, "parent_id": str(ObjectId())}, + parent_catalogue_category_in_data=CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_B, + ) + self.call_create() + self.check_create_success() -def test_delete_with_non_existent_id(test_helpers, database_mock, catalogue_category_repository): - """ - Test deleting a catalogue category with a non-existent ID. + def test_create_with_non_existent_parent_id(self): + """Test creating a catalogue category with a non existent parent_id""" - Verify that the `delete` method properly handles the deletion of a catalogue category with a non-existent ID. - """ - catalogue_category_id = str(ObjectId()) + parent_id = str(ObjectId()) - # Mock `delete_one` to return that no document has been deleted - test_helpers.mock_delete_one(database_mock.catalogue_categories, 0) + self.mock_create( + {**CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, "parent_id": parent_id}, + parent_catalogue_category_in_data=None, + ) + self.call_create_expecting_error(MissingRecordError) + self.check_create_failed_with_exception(f"No parent catalogue category found with ID: {parent_id}") - # Mock `find_one` to return no child catalogue category document - test_helpers.mock_find_one(database_mock.catalogue_items, None) - test_helpers.mock_find_one(database_mock.catalogue_categories, None) + def test_create_with_duplicate_name_within_parent(self): + """Test creating a catalogue category with a duplicate catalogue category being found in the same parent + catalogue category""" - with pytest.raises(MissingRecordError) as exc: - catalogue_category_repository.delete(catalogue_category_id) - assert str(exc.value) == f"No catalogue category found with ID: {catalogue_category_id}" - database_mock.catalogue_categories.delete_one.assert_called_once_with( - {"_id": CustomObjectId(catalogue_category_id)}, session=None - ) + self.mock_create( + {**CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, "parent_id": str(ObjectId())}, + parent_catalogue_category_in_data=CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_B, + duplicate_catalogue_category_in_data=CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, + ) + self.call_create_expecting_error(DuplicateRecordError) + self.check_create_failed_with_exception( + "Duplicate catalogue category found within the parent catalogue category" + ) -def test_get(test_helpers, database_mock, catalogue_category_repository): - """ - Test getting a catalogue category. +class GetDSL(CatalogueCategoryRepoDSL): + """Base class for get tests""" - Verify that the `get` method properly handles the retrieval of a catalogue category by ID. - """ - # pylint: disable=duplicate-code - catalogue_category = CatalogueCategoryOut( - id=str(ObjectId()), - name="Category A", - code="category-a", - is_leaf=False, - parent_id=None, - properties=[], - **MOCK_CREATED_MODIFIED_TIME, - ) - session = MagicMock() + _obtained_catalogue_category_id: str + _expected_catalogue_category_out: Optional[CatalogueCategoryOut] + _obtained_catalogue_category: Optional[CatalogueCategoryOut] + _get_exception: pytest.ExceptionInfo + + def mock_get(self, catalogue_category_id: str, catalogue_category_in_data: Optional[dict]): + """Mocks database methods appropriately to test the 'get' repo method + + :param catalogue_category_id: ID of the catalogue category that will be obtained + :param catalogue_category_in_data: Either None or a Dictionary containing the catalogue category data as would + be required for a CatalogueCategoryIn database model (i.e. No id or created + and modified times required) + """ + + self._expected_catalogue_category_out = ( + CatalogueCategoryOut( + **CatalogueCategoryIn(**catalogue_category_in_data).model_dump(by_alias=True), + id=CustomObjectId(catalogue_category_id), + ) + if catalogue_category_in_data + else None + ) + + RepositoryTestHelpers.mock_find_one( + self.catalogue_categories_collection, + self._expected_catalogue_category_out.model_dump() if self._expected_catalogue_category_out else None, + ) + + def call_get(self, catalogue_category_id: str): + """Calls the CatalogueCategoryRepo `get` method with the appropriate data from a prior call to `mock_get`""" + + self._obtained_catalogue_category_id = catalogue_category_id + self._obtained_catalogue_category = self.catalogue_category_repository.get( + catalogue_category_id, session=self.mock_session + ) + + def call_get_expecting_error(self, catalogue_category_id: str, error_type: type[BaseException]): + """Calls the CatalogueCategoryRepo `get` method with the appropriate data from a prior call to `mock_get` + while expecting an error to be raised""" + + with pytest.raises(error_type) as exc: + self.catalogue_category_repository.get(catalogue_category_id) + self._get_exception = exc + + def check_get_success(self): + """Checks that a prior call to `call_get` worked as expected""" + + self.catalogue_categories_collection.find_one.assert_called_once_with( + {"_id": CustomObjectId(self._obtained_catalogue_category_id)}, session=self.mock_session + ) + assert self._obtained_catalogue_category == self._expected_catalogue_category_out + + def check_get_failed_with_exception(self, message: str): + """Checks that a prior call to `call_get_expecting_error` worked as expected, raising an exception + with the correct message""" + + self.catalogue_categories_collection.find_one.assert_not_called() + + assert str(self._get_exception.value) == message + + +class TestGet(GetDSL): + """Tests for getting a catalogue category""" + + def test_get(self): + """Test getting a catalogue category""" + + catalogue_category_id = str(ObjectId()) + + self.mock_get(catalogue_category_id, CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A) + self.call_get(catalogue_category_id) + self.check_get_success() + + def test_get_with_non_existent_id(self): + """Test getting a catalogue category with a non-existent ID""" + + catalogue_category_id = str(ObjectId()) + + self.mock_get(catalogue_category_id, None) + self.call_get(catalogue_category_id) + self.check_get_success() + + def test_get_with_invalid_id(self): + """Test getting a catalogue category with an invalid ID""" + + catalogue_category_id = "invalid-id" + + self.call_get_expecting_error(catalogue_category_id, InvalidObjectIdError) + self.check_get_failed_with_exception("Invalid ObjectId value 'invalid-id'") + + +# pylint: disable=duplicate-code +class GetBreadcrumbsDSL(CatalogueCategoryRepoDSL): + """Base class for breadcrumbs tests""" + + _breadcrumbs_query_result: list[dict] + _mock_aggregation_pipeline = MagicMock() + _expected_breadcrumbs: MagicMock + _obtained_catalogue_category_id: str + _obtained_breadcrumbs: MagicMock # pylint: enable=duplicate-code - # Mock `find_one` to return a catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **MOCK_CREATED_MODIFIED_TIME, - "_id": CustomObjectId(catalogue_category.id), - "name": catalogue_category.name, - "code": catalogue_category.code, - "is_leaf": catalogue_category.is_leaf, - "parent_id": catalogue_category.parent_id, - "properties": catalogue_category.properties, - }, - ) - - retrieved_catalogue_category = catalogue_category_repository.get(catalogue_category.id, session=session) - - database_mock.catalogue_categories.find_one.assert_called_once_with( - {"_id": CustomObjectId(catalogue_category.id)}, session=session - ) - assert retrieved_catalogue_category == catalogue_category - - -def test_get_with_invalid_id(catalogue_category_repository): - """ - Test getting a catalogue category with an invalid ID. - - Verify that the `get` method properly handles the retrieval of a catalogue category with an invalid ID. - """ - with pytest.raises(InvalidObjectIdError) as exc: - catalogue_category_repository.get("invalid") - assert str(exc.value) == "Invalid ObjectId value 'invalid'" - - -def test_get_with_non_existent_id(test_helpers, database_mock, catalogue_category_repository): - """ - Test getting a catalogue category with a non-existent ID. - - Verify that the `get` method properly handles the retrieval of a catalogue category with a non-existent ID. - """ - catalogue_category_id = str(ObjectId()) - - # Mock `find_one` to not return a catalogue category document - test_helpers.mock_find_one(database_mock.catalogue_categories, None) - - retrieved_catalogue_category = catalogue_category_repository.get(catalogue_category_id) - - assert retrieved_catalogue_category is None - database_mock.catalogue_categories.find_one.assert_called_once_with( - {"_id": CustomObjectId(catalogue_category_id)}, session=None - ) - - -@patch("inventory_management_system_api.repositories.catalogue_category.utils") -def test_get_breadcrumbs(mock_utils, database_mock, catalogue_category_repository): - """ - Test getting breadcrumbs for a specific catalogue category - - Verify that the 'get_breadcrumbs' method properly handles the retrieval of breadcrumbs for a catalogue - category - """ - catalogue_category_id = str(ObjectId()) - mock_aggregation_pipeline = MagicMock() - mock_breadcrumbs = MagicMock() - - mock_utils.create_breadcrumbs_aggregation_pipeline.return_value = mock_aggregation_pipeline - mock_utils.compute_breadcrumbs.return_value = mock_breadcrumbs - database_mock.catalogue_categories.aggregate.return_value = MOCK_BREADCRUMBS_QUERY_RESULT_LESS_THAN_MAX_LENGTH - - retrieved_breadcrumbs = catalogue_category_repository.get_breadcrumbs(catalogue_category_id) - - mock_utils.create_breadcrumbs_aggregation_pipeline.assert_called_once_with( - entity_id=catalogue_category_id, collection_name="catalogue_categories" - ) - mock_utils.compute_breadcrumbs.assert_called_once_with( - list(MOCK_BREADCRUMBS_QUERY_RESULT_LESS_THAN_MAX_LENGTH), - entity_id=catalogue_category_id, - collection_name="catalogue_categories", - ) - assert retrieved_breadcrumbs == mock_breadcrumbs - - -def test_list(test_helpers, database_mock, catalogue_category_repository): - """ - Test getting catalogue categories. - - Verify that the `list` method properly handles the retrieval of catalogue categories without filters. - """ + def mock_breadcrumbs(self, breadcrumbs_query_result: list[dict]): + """Mocks database methods appropriately to test the 'get_breadcrumbs' repo method + + :param breadcrumbs_query_result: List of dictionaries containing the breadcrumbs query result from the + aggregation pipeline + """ + + self._breadcrumbs_query_result = breadcrumbs_query_result + self._mock_aggregation_pipeline = MagicMock() + self._expected_breadcrumbs = MagicMock() + + self.mock_utils.create_breadcrumbs_aggregation_pipeline.return_value = self._mock_aggregation_pipeline + self.catalogue_categories_collection.aggregate.return_value = breadcrumbs_query_result + self.mock_utils.compute_breadcrumbs.return_value = self._expected_breadcrumbs + + def call_get_breadcrumbs(self, catalogue_category_id: str): + """Calls the CatalogueCategoryRepo `get_breadcrumbs` method""" + + self._obtained_catalogue_category_id = catalogue_category_id + self._obtained_breadcrumbs = self.catalogue_category_repository.get_breadcrumbs( + catalogue_category_id, session=self.mock_session + ) + + def check_get_breadcrumbs_success(self): + """Checks that a prior call to `call_get_breadcrumbs` worked as expected""" + + self.mock_utils.create_breadcrumbs_aggregation_pipeline.assert_called_once_with( + entity_id=self._obtained_catalogue_category_id, collection_name="catalogue_categories" + ) + self.catalogue_categories_collection.aggregate.assert_called_once_with( + self._mock_aggregation_pipeline, session=self.mock_session + ) + self.mock_utils.compute_breadcrumbs.assert_called_once_with( + list(self._breadcrumbs_query_result), + entity_id=self._obtained_catalogue_category_id, + collection_name="catalogue_categories", + ) + + assert self._obtained_breadcrumbs == self._expected_breadcrumbs + + +# pylint: disable=duplicate-code +class TestGetBreadcrumbs(GetBreadcrumbsDSL): + """Tests for getting the breadcrumbs of a catalogue category""" + + def test_get_breadcrumbs(self): + """Test getting a catalogue category's breadcrumbs""" + + self.mock_breadcrumbs(MOCK_BREADCRUMBS_QUERY_RESULT_LESS_THAN_MAX_LENGTH) + self.call_get_breadcrumbs(str(ObjectId())) + self.check_get_breadcrumbs_success() + + +# pylint: enable=duplicate-code + + +class ListDSL(CatalogueCategoryRepoDSL): + """Base class for list tests""" + + _expected_catalogue_categories_out: list[CatalogueCategoryOut] + _parent_id_filter: Optional[str] + _obtained_catalogue_categories_out: list[CatalogueCategoryOut] + + def mock_list(self, catalogue_categories_in_data: list[dict]): + """Mocks database methods appropriately to test the 'list' repo method + + :param catalogue_categories_in_data: List of dictionaries containing the catalogue category data as would be + required for a CatalogueCategoryIn database model (i.e. no id or created + and modified times required) + """ + + self._expected_catalogue_categories_out = [ + CatalogueCategoryOut( + **CatalogueCategoryIn(**catalogue_category_in_data).model_dump(by_alias=True), id=ObjectId() + ) + for catalogue_category_in_data in catalogue_categories_in_data + ] + + RepositoryTestHelpers.mock_find( + self.catalogue_categories_collection, + [catalogue_category_out.model_dump() for catalogue_category_out in self._expected_catalogue_categories_out], + ) + + def call_list(self, parent_id: Optional[str]): + """Calls the CatalogueCategoryRepo `list` method""" + + self._parent_id_filter = parent_id + + self._obtained_catalogue_categories_out = self.catalogue_category_repository.list( + parent_id=parent_id, session=self.mock_session + ) + + def check_list_success(self): + """Checks that a prior call to 'call_list` worked as expected""" + + self.mock_utils.list_query.assert_called_once_with(self._parent_id_filter, "catalogue categories") + self.catalogue_categories_collection.find.assert_called_once_with( + self.mock_utils.list_query.return_value, session=self.mock_session + ) + + assert self._obtained_catalogue_categories_out == self._expected_catalogue_categories_out + + +class TestList(ListDSL): + """Tests for listing Catalogue Categorie's""" + + def test_list(self): + """Test listing all Catalogue Categories""" + + self.mock_list( + [CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_B] + ) + self.call_list(parent_id=None) + self.check_list_success() + + def test_list_with_parent_id_filter(self): + """Test listing all Catalogue Categories with a given parent_id""" + + self.mock_list( + [CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_B] + ) + self.call_list(parent_id=str(ObjectId())) + self.check_list_success() + + def test_list_with_null_parent_id_filter(self): + """Test listing all Catalogue Categories with a 'null' parent_id""" + + self.mock_list( + [CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_B] + ) + self.call_list(parent_id="null") + self.check_list_success() + # pylint: disable=duplicate-code - catalogue_category_a = CatalogueCategoryOut( - id=str(ObjectId()), - name="Category A", - code="category-a", - is_leaf=False, - parent_id=None, - properties=[], - **MOCK_CREATED_MODIFIED_TIME, - ) - - catalogue_category_b = CatalogueCategoryOut( - id=str(ObjectId()), - name="Category B", - code="category-b", - is_leaf=False, - parent_id=None, - properties=[], - **MOCK_CREATED_MODIFIED_TIME, - ) - session = MagicMock() + def test_list_with_parent_id_with_no_results(self): + """Test listing all Catalogue Categories with a parent_id filter returning no results""" + + self.mock_list([]) + self.call_list(parent_id=str(ObjectId())) + self.check_list_success() + # pylint: enable=duplicate-code - # Mock `find` to return a list of catalogue category documents - test_helpers.mock_find( - database_mock.catalogue_categories, - [ + +class UpdateDSL(CatalogueCategoryRepoDSL): + """Base class for update tests""" + + # pylint:disable=too-many-instance-attributes + _catalogue_category_in: CatalogueCategoryIn + _stored_catalogue_category_out: Optional[CatalogueCategoryOut] + _expected_catalogue_category_out: CatalogueCategoryOut + _updated_catalogue_category_id: str + _updated_catalogue_category: CatalogueCategoryOut + _moving_catalogue_category: bool + _update_exception: pytest.ExceptionInfo + + def set_update_data(self, new_catalogue_category_in_data: dict): + """Assigns the update data to use during a call to `call_update` + + :param new_catalogue_category_in_data: New catalogue category data as would be required for a + CatalogueCategoryIn database model to supply to the SystemRepo + `update` method + """ + self._catalogue_category_in = CatalogueCategoryIn(**new_catalogue_category_in_data) + + # pylint:disable=too-many-arguments + def mock_update( + self, + catalogue_category_id: str, + new_catalogue_category_in_data: dict, + stored_catalogue_category_in_data: Optional[dict], + new_parent_catalogue_category_in_data: Optional[dict] = None, + duplicate_catalogue_category_in_data: Optional[dict] = None, + valid_move_result: bool = True, + ): + """Mocks database methods appropriately to test the 'update' repo method + + :param catalogue_category_id: ID of the catalogue category that will be obtained + :param new_catalogue_category_in_data: Dictionary containing the new catalogue category data as would be + required for a CatalogueCategoryIn database model (i.e. no id or + created and modified times required) + :param stored_catalogue_category_in_data: Dictionary containing the catalogue category data for the existing + stored catalogue category as would be required for a + CatalogueCategoryIn database model + :param new_parent_catalogue_category_in_data: Either None or a dictionary containing the new parent catalogue + category data as would be required for a CatalogueCategoryIn + database model + :param duplicate_catalogue_category_in_data: Either None or a dictionary containing the data for a duplicate + catalogue category as would be required for a CatalogueCategoryIn + database model + :param valid_move_result: Whether to mock in a valid or invalid move result i.e. when True will simulating + moving the catalogue category to one of its own children + """ + self.set_update_data(new_catalogue_category_in_data) + + # When a parent_id is given, need to mock the find_one for it too + if new_catalogue_category_in_data["parent_id"]: + # If new_parent_catalogue_category_data is given as none, then it is intentionally supposed to be, otherwise + # pass through CatalogueCategoryIn first to ensure it has creation and modified times + RepositoryTestHelpers.mock_find_one( + self.catalogue_categories_collection, + ( + { + **CatalogueCategoryIn(**new_parent_catalogue_category_in_data).model_dump(by_alias=True), + "_id": new_catalogue_category_in_data["parent_id"], + } + if new_parent_catalogue_category_in_data + else None + ), + ) + + # Stored catalogue category + self._stored_catalogue_category_out = ( + CatalogueCategoryOut( + **CatalogueCategoryIn(**stored_catalogue_category_in_data).model_dump(by_alias=True), + id=CustomObjectId(catalogue_category_id), + ) + if stored_catalogue_category_in_data + else None + ) + RepositoryTestHelpers.mock_find_one( + self.catalogue_categories_collection, + self._stored_catalogue_category_out.model_dump() if self._stored_catalogue_category_out else None, + ) + + # Duplicate check + self._moving_catalogue_category = stored_catalogue_category_in_data is not None and ( + new_catalogue_category_in_data["parent_id"] != stored_catalogue_category_in_data["parent_id"] + ) + if ( + self._stored_catalogue_category_out + and (self._catalogue_category_in.name != self._stored_catalogue_category_out.name) + ) or self._moving_catalogue_category: + RepositoryTestHelpers.mock_find_one( + self.catalogue_categories_collection, + ( + { + **CatalogueCategoryIn(**duplicate_catalogue_category_in_data).model_dump(by_alias=True), + "_id": ObjectId(), + } + if duplicate_catalogue_category_in_data + else None + ), + ) + + # Final catalogue category after update + self._expected_catalogue_category_out = CatalogueCategoryOut( + **self._catalogue_category_in.model_dump(), id=CustomObjectId(catalogue_category_id) + ) + RepositoryTestHelpers.mock_find_one( + self.catalogue_categories_collection, self._expected_catalogue_category_out.model_dump() + ) + + if self._moving_catalogue_category: + mock_aggregation_pipeline = MagicMock() + self.mock_utils.create_move_check_aggregation_pipeline.return_value = mock_aggregation_pipeline + if valid_move_result: + self.mock_utils.is_valid_move_result.return_value = True + self.catalogue_categories_collection.aggregate.return_value = MOCK_MOVE_QUERY_RESULT_VALID + else: + self.mock_utils.is_valid_move_result.return_value = False + self.catalogue_categories_collection.aggregate.return_value = MOCK_MOVE_QUERY_RESULT_INVALID + + def call_update(self, catalogue_category_id: str): + """Calls the CatalogueCategoryRepo `update` method with the appropriate data from a prior call to `mock_update` + (or`set_update_data`)""" + + self._updated_catalogue_category_id = catalogue_category_id + self._updated_catalogue_category = self.catalogue_category_repository.update( + catalogue_category_id, self._catalogue_category_in, session=self.mock_session + ) + + def call_update_expecting_error(self, catalogue_category_id: str, error_type: type[BaseException]): + """Calls the CatalogueCategoryRepo `update` method with the appropriate data from a prior call to `mock_update` + (or `set_update_data`) while expecting an error to be raised""" + + with pytest.raises(error_type) as exc: + self.catalogue_category_repository.update(catalogue_category_id, self._catalogue_category_in) + self._update_exception = exc + + def check_update_success(self): + """Checks that a prior call to `call_update` worked as expected""" + + # Obtain a list of expected find_one calls + expected_find_one_calls = [] + + # Parent existence check + if self._catalogue_category_in.parent_id: + expected_find_one_calls.append( + call({"_id": self._catalogue_category_in.parent_id}, session=self.mock_session) + ) + + # Stored catalogue category + expected_find_one_calls.append( + call( + {"_id": CustomObjectId(self._expected_catalogue_category_out.id)}, + session=self.mock_session, + ) + ) + + # Duplicate check (which only runs if moving or changing the name) + if ( + self._stored_catalogue_category_out + and (self._catalogue_category_in.name != self._stored_catalogue_category_out.name) + ) or self._moving_catalogue_category: + expected_find_one_calls.append( + call( + { + "parent_id": self._catalogue_category_in.parent_id, + "code": self._catalogue_category_in.code, + "_id": {"$ne": CustomObjectId(self._updated_catalogue_category_id)}, + }, + session=self.mock_session, + ) + ) + self.catalogue_categories_collection.find_one.assert_has_calls(expected_find_one_calls) + + if self._moving_catalogue_category: + self.mock_utils.create_move_check_aggregation_pipeline.assert_called_once_with( + entity_id=self._updated_catalogue_category_id, + destination_id=str(self._catalogue_category_in.parent_id), + collection_name="catalogue_categories", + ) + self.catalogue_categories_collection.aggregate.assert_called_once_with( + self.mock_utils.create_move_check_aggregation_pipeline.return_value, session=self.mock_session + ) + + self.catalogue_categories_collection.update_one.assert_called_once_with( { - **MOCK_CREATED_MODIFIED_TIME, - "_id": CustomObjectId(catalogue_category_a.id), - "name": catalogue_category_a.name, - "code": catalogue_category_a.code, - "is_leaf": catalogue_category_a.is_leaf, - "parent_id": catalogue_category_a.parent_id, - "properties": catalogue_category_a.properties, + "_id": CustomObjectId(self._updated_catalogue_category_id), }, { - **MOCK_CREATED_MODIFIED_TIME, - "_id": CustomObjectId(catalogue_category_b.id), - "name": catalogue_category_b.name, - "code": catalogue_category_b.code, - "is_leaf": catalogue_category_b.is_leaf, - "parent_id": catalogue_category_b.parent_id, - "properties": catalogue_category_b.properties, + "$set": { + **self._catalogue_category_in.model_dump(), + }, }, - ], - ) + session=self.mock_session, + ) - retrieved_catalogue_categories = catalogue_category_repository.list(None, session=session) + assert self._updated_catalogue_category == self._expected_catalogue_category_out - database_mock.catalogue_categories.find.assert_called_once_with({}, session=session) - assert retrieved_catalogue_categories == [catalogue_category_a, catalogue_category_b] + def check_update_failed_with_exception(self, message: str): + """Checks that a prior call to `call_update_expecting_error` worked as expected, raising an exception + with the correct message""" + self.catalogue_categories_collection.update_one.assert_not_called() -def test_list_with_parent_id_filter(test_helpers, database_mock, catalogue_category_repository): - """ - Test getting catalogue categories based on the provided parent_id filter. + assert str(self._update_exception.value) == message - Verify that the `list` method properly handles the retrieval of catalogue categories based on the provided - parent_id filter. - """ - # pylint: disable=duplicate-code - catalogue_category = CatalogueCategoryOut( - id=str(ObjectId()), - name="Category A", - code="category-a", - is_leaf=False, - parent_id=None, - properties=[], - **MOCK_CREATED_MODIFIED_TIME, - ) - session = MagicMock() - # pylint: enable=duplicate-code - # Mock `find` to return a list of catalogue category documents - test_helpers.mock_find( - database_mock.catalogue_categories, - [ - { - **MOCK_CREATED_MODIFIED_TIME, - "_id": CustomObjectId(catalogue_category.id), - "name": catalogue_category.name, - "code": catalogue_category.code, - "is_leaf": catalogue_category.is_leaf, - "parent_id": catalogue_category.parent_id, - "properties": catalogue_category.properties, - } - ], - ) - - parent_id = ObjectId() - retrieved_catalogue_categories = catalogue_category_repository.list(str(parent_id), session=session) - - database_mock.catalogue_categories.find.assert_called_once_with({"parent_id": parent_id}, session=session) - assert retrieved_catalogue_categories == [catalogue_category] - - -def test_list_with_null_parent_id_filter(test_helpers, database_mock, catalogue_category_repository): - """ - Test getting catalogue categories when the provided parent_id filter is "null" - - Verify that the `list` method properly handles the retrieval of catalogue categories based on the provided parent - parent_id filter. - """ - # pylint: disable=duplicate-code - catalogue_category_a = CatalogueCategoryOut( - id=str(ObjectId()), - name="Category A", - code="category-a", - is_leaf=False, - parent_id=None, - properties=[], - **MOCK_CREATED_MODIFIED_TIME, - ) - - catalogue_category_b = CatalogueCategoryOut( - id=str(ObjectId()), - name="Category B", - code="category-b", - is_leaf=False, - parent_id=None, - properties=[], - **MOCK_CREATED_MODIFIED_TIME, - ) - session = MagicMock() - # pylint: enable=duplicate-code +class TestUpdate(UpdateDSL): + """Tests for updating a catalogue category""" - # Mock `find` to return a list of catalogue category documents - test_helpers.mock_find( - database_mock.catalogue_categories, - [ - { - **MOCK_CREATED_MODIFIED_TIME, - "_id": CustomObjectId(catalogue_category_a.id), - "name": catalogue_category_a.name, - "code": catalogue_category_a.code, - "is_leaf": catalogue_category_a.is_leaf, - "parent_id": catalogue_category_a.parent_id, - "properties": catalogue_category_a.properties, + def test_update(self): + """Test updating a catalogue category""" + + catalogue_category_id = str(ObjectId()) + + self.mock_update( + catalogue_category_id, + CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, + CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_B, + ) + self.call_update(catalogue_category_id) + self.check_update_success() + + def test_update_no_changes(self): + """Test updating a catalogue category to have exactly the same contents""" + + catalogue_category_id = str(ObjectId()) + + self.mock_update( + catalogue_category_id, + CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, + CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, + ) + self.call_update(catalogue_category_id) + self.check_update_success() + + def test_update_parent_id(self): + """Test updating a catalogue category's parent_id to move it""" + + catalogue_category_id = str(ObjectId()) + + self.mock_update( + catalogue_category_id=catalogue_category_id, + new_catalogue_category_in_data={ + **CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, + "parent_id": str(ObjectId()), }, - { - **MOCK_CREATED_MODIFIED_TIME, - "_id": CustomObjectId(catalogue_category_b.id), - "name": catalogue_category_b.name, - "code": catalogue_category_b.code, - "is_leaf": catalogue_category_b.is_leaf, - "parent_id": catalogue_category_b.parent_id, - "properties": catalogue_category_b.properties, + stored_catalogue_category_in_data=CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, + new_parent_catalogue_category_in_data=CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_B, + duplicate_catalogue_category_in_data=None, + valid_move_result=True, + ) + self.call_update(catalogue_category_id) + self.check_update_success() + + def test_update_parent_id_to_child_of_self(self): + """Test updating a catalogue category's parent_id to a child of it self (should prevent this)""" + + catalogue_category_id = str(ObjectId()) + + self.mock_update( + catalogue_category_id=catalogue_category_id, + new_catalogue_category_in_data={ + **CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, + "parent_id": str(ObjectId()), }, - ], - ) + stored_catalogue_category_in_data=CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_B, + new_parent_catalogue_category_in_data=CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_B, + duplicate_catalogue_category_in_data=None, + valid_move_result=False, + ) + self.call_update_expecting_error(catalogue_category_id, InvalidActionError) + self.check_update_failed_with_exception("Cannot move a catalogue category to one of its own children") - retrieved_catalogue_categories = catalogue_category_repository.list("null", session=session) + def test_update_with_non_existent_parent_id(self): + """Test updating a catalogue category's parent_id to a non-existent catalogue category""" - database_mock.catalogue_categories.find.assert_called_once_with({"parent_id": None}, session=session) - assert retrieved_catalogue_categories == [catalogue_category_a, catalogue_category_b] + catalogue_category_id = str(ObjectId()) + new_parent_id = str(ObjectId()) + self.mock_update( + catalogue_category_id, + {**CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, "parent_id": new_parent_id}, + CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, + new_parent_catalogue_category_in_data=None, + ) + self.call_update_expecting_error(catalogue_category_id, MissingRecordError) + self.check_update_failed_with_exception(f"No parent catalogue category found with ID: {new_parent_id}") -def test_list_with_parent_id_filter_no_matching_results(test_helpers, database_mock, catalogue_category_repository): - """ - Test getting catalogue categories based on the provided parent_id filter when there is no matching - results in the database. + def test_update_name_to_duplicate_within_parent(self): + """Test updating a catalogue category's name to one that is a duplicate within the same parent Catalogue + Category""" - Verify that the `list` method properly handles the retrieval of catalogue categories based on the provided - parent_id filter when there are no matching results in the database - """ - session = MagicMock() + catalogue_category_id = str(ObjectId()) + new_name = "New Duplicate Name" - # Mock `find` to return an empty list of catalogue category documents - test_helpers.mock_find(database_mock.catalogue_categories, []) + self.mock_update( + catalogue_category_id, + {**CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, "name": new_name}, + CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, + duplicate_catalogue_category_in_data=CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, + ) + self.call_update_expecting_error(catalogue_category_id, DuplicateRecordError) + self.check_update_failed_with_exception( + "Duplicate catalogue category found within the parent catalogue category" + ) - parent_id = ObjectId() - retrieved_catalogue_categories = catalogue_category_repository.list(str(parent_id), session=session) + def test_update_parent_id_with_duplicate_within_parent(self): + """Test updating a catalogue category's parent-id to one contains a catalogue category with a duplicate name + within the same parent catalogue category""" - database_mock.catalogue_categories.find.assert_called_once_with({"parent_id": parent_id}, session=session) - assert retrieved_catalogue_categories == [] + catalogue_category_id = str(ObjectId()) + new_parent_id = str(ObjectId()) + self.mock_update( + catalogue_category_id, + {**CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, "parent_id": new_parent_id}, + CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, + new_parent_catalogue_category_in_data=CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_B, + duplicate_catalogue_category_in_data=CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, + ) + self.call_update_expecting_error(catalogue_category_id, DuplicateRecordError) + self.check_update_failed_with_exception( + "Duplicate catalogue category found within the parent catalogue category" + ) -# pylint:disable=W0613 -def test_list_with_invalid_parent_id_filter(test_helpers, database_mock, catalogue_category_repository): - """ - Test getting catalogue_categories when given an invalid parent_id to filter on + def test_update_with_invalid_id(self): + """Test updating a catalogue category with an invalid id""" - Verify that the `list` method properly handles the retrieval of catalogue categories when given an invalid - parent_id filter - """ - with pytest.raises(InvalidObjectIdError) as exc: - catalogue_category_repository.list("invalid") - database_mock.catalogue_categories.find.assert_not_called() - assert str(exc.value) == "Invalid ObjectId value 'invalid'" + catalogue_category_id = "invalid-id" + self.set_update_data(CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A) + self.call_update_expecting_error(catalogue_category_id, InvalidObjectIdError) + self.check_update_failed_with_exception("Invalid ObjectId value 'invalid-id'") -def test_update(test_helpers, database_mock, catalogue_category_repository): - """ - Test updating a catalogue category. - Verify that the `update` method properly handles the catalogue category to be updated, checks that the catalogue - category does not have child elements, there is not a duplicate catalogue category, and updates the catalogue - category. - """ - # pylint: disable=duplicate-code - catalogue_category = CatalogueCategoryOut( - id=str(ObjectId()), - name="Category B", - code="category-b", - is_leaf=False, - parent_id=None, - properties=[], - **MOCK_CREATED_MODIFIED_TIME, - ) - session = MagicMock() - # pylint: enable=duplicate-code +class HasChildElementsDSL(CatalogueCategoryRepoDSL): + """Base class for has_child_elements tests""" - # Mock `find_one` to return a catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **CATALOGUE_CATEGORY_INFO, - **MOCK_CREATED_MODIFIED_TIME, - "_id": CustomObjectId(catalogue_category.id), - "is_leaf": catalogue_category.is_leaf, - "parent_id": catalogue_category.parent_id, - "properties": catalogue_category.properties, - }, - ) - # Mock `find_one` to return no duplicate catalogue categories found - test_helpers.mock_find_one(database_mock.catalogue_categories, None) - # Mock `update_one` to return an object for the updated catalogue category document - test_helpers.mock_update_one(database_mock.catalogue_categories) - # pylint: disable=duplicate-code - # Mock `find_one` to return the updated catalogue category document - catalogue_category_in = CatalogueCategoryIn( - **MOCK_CREATED_MODIFIED_TIME, - name=catalogue_category.name, - code=catalogue_category.code, - is_leaf=catalogue_category.is_leaf, - parent_id=catalogue_category.parent_id, - properties=catalogue_category.properties, - ) - # pylint: enable=duplicate-code - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **catalogue_category_in.model_dump(by_alias=True), - "_id": CustomObjectId(catalogue_category.id), - }, - ) - - updated_catalogue_category = catalogue_category_repository.update( - catalogue_category.id, catalogue_category_in, session=session - ) - - database_mock.catalogue_categories.update_one.assert_called_once_with( - {"_id": CustomObjectId(catalogue_category.id)}, - { - "$set": { - **catalogue_category_in.model_dump(by_alias=True), - } - }, - session=session, - ) - database_mock.catalogue_categories.find_one.assert_has_calls( - [ - call({"_id": CustomObjectId(catalogue_category.id)}, session=session), - call( - { - "parent_id": catalogue_category.parent_id, - "code": catalogue_category.code, - "_id": {"$ne": CustomObjectId(catalogue_category.id)}, - }, - session=session, - ), - call({"_id": CustomObjectId(catalogue_category.id)}, session=session), - ] - ) - assert updated_catalogue_category == CatalogueCategoryOut( - id=catalogue_category.id, **catalogue_category_in.model_dump(by_alias=True) - ) - - -@patch("inventory_management_system_api.repositories.catalogue_category.utils") -def test_update_parent_id(utils_mock, test_helpers, database_mock, catalogue_category_repository): - """ - Test updating a catalogue category's parent_id - - Verify that the `update` method properly handles the update of a catalogue category when the - parent_id changes - """ - parent_catalogue_category_id = str(ObjectId()) - catalogue_category = CatalogueCategoryOut( - id=str(ObjectId()), - **{**CATALOGUE_CATEGORY_INFO, "parent_id": parent_catalogue_category_id, **MOCK_CREATED_MODIFIED_TIME}, - ) - session = MagicMock() - new_parent_id = str(ObjectId()) - expected_catalogue_category = CatalogueCategoryOut( - **{**catalogue_category.model_dump(), "parent_id": new_parent_id} - ) - - # Mock `find_one` to return a parent catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **CATALOGUE_CATEGORY_INFO, - **MOCK_CREATED_MODIFIED_TIME, - "_id": CustomObjectId(new_parent_id), - }, - ) - # Mock `find_one` to return the stored catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - catalogue_category.model_dump(), - ) - # Mock `find_one` to return no duplicate catalogue categories found - test_helpers.mock_find_one(database_mock.catalogue_categories, None) - # Mock `update_one` to return an object for the updated catalogue category document - test_helpers.mock_update_one(database_mock.catalogue_categories) - # Mock `find_one` to return the updated catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - {**catalogue_category.model_dump(), "parent_id": CustomObjectId(new_parent_id)}, - ) - - # Mock utils so not moving to a child of itself - mock_aggregation_pipeline = MagicMock() - utils_mock.create_move_check_aggregation_pipeline.return_value = mock_aggregation_pipeline - utils_mock.is_valid_move_result.return_value = True - database_mock.catalogue_categories.aggregate.return_value = MOCK_MOVE_QUERY_RESULT_VALID - - catalogue_category_in = CatalogueCategoryIn( - **{**CATALOGUE_CATEGORY_INFO, "parent_id": new_parent_id, **MOCK_CREATED_MODIFIED_TIME} - ) - updated_catalogue_category = catalogue_category_repository.update( - catalogue_category.id, catalogue_category_in, session=session - ) - - utils_mock.create_move_check_aggregation_pipeline.assert_called_once_with( - entity_id=catalogue_category.id, destination_id=new_parent_id, collection_name="catalogue_categories" - ) - database_mock.catalogue_categories.aggregate.assert_called_once_with(mock_aggregation_pipeline, session=session) - utils_mock.is_valid_move_result.assert_called_once() - - database_mock.catalogue_categories.update_one.assert_called_once_with( - {"_id": CustomObjectId(catalogue_category.id)}, - {"$set": {**catalogue_category_in.model_dump(by_alias=True)}}, - session=session, - ) - database_mock.catalogue_categories.find_one.assert_has_calls( - [ - call({"_id": CustomObjectId(new_parent_id)}, session=session), - call({"_id": CustomObjectId(catalogue_category.id)}, session=session), - call( - { - "parent_id": CustomObjectId(new_parent_id), - "code": catalogue_category.code, - "_id": {"$ne": CustomObjectId(catalogue_category.id)}, - }, - session=session, - ), - call({"_id": CustomObjectId(catalogue_category.id)}, session=session), - ] - ) - assert updated_catalogue_category == expected_catalogue_category - - -@patch("inventory_management_system_api.repositories.catalogue_category.utils") -def test_update_parent_id_moving_to_child(utils_mock, test_helpers, database_mock, catalogue_category_repository): - """ - Test updating a catalogue category's parent_id when moving to a child of itself - - Verify that the `update` method properly handles the update of a catalogue category when the new - parent_id is a child of itself - """ - parent_catalogue_category_id = str(ObjectId()) - catalogue_category = CatalogueCategoryOut( - id=str(ObjectId()), - **{**CATALOGUE_CATEGORY_INFO, "parent_id": parent_catalogue_category_id, **MOCK_CREATED_MODIFIED_TIME}, - ) - session = MagicMock() - new_parent_id = str(ObjectId()) - - # Mock `find_one` to return a parent catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **CATALOGUE_CATEGORY_INFO, - **MOCK_CREATED_MODIFIED_TIME, - "_id": CustomObjectId(new_parent_id), - }, - ) - # Mock `find_one` to return the stored catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - catalogue_category.model_dump(), - ) - # Mock `find_one` to return no duplicate catalogue categories found - test_helpers.mock_find_one(database_mock.catalogue_categories, None) - # Mock `update_one` to return an object for the updated catalogue category document - test_helpers.mock_update_one(database_mock.catalogue_categories) - # Mock `find_one` to return the updated catalogue category document - catalogue_category_in = CatalogueCategoryIn( - **{**CATALOGUE_CATEGORY_INFO, "parent_id": new_parent_id, **MOCK_CREATED_MODIFIED_TIME} - ) - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **catalogue_category_in.model_dump(by_alias=True), - "_id": CustomObjectId(catalogue_category.id), - "parent_id": CustomObjectId(new_parent_id), - }, - ) - - # Mock utils so not moving to a child of itself - mock_aggregation_pipeline = MagicMock() - utils_mock.create_move_check_aggregation_pipeline.return_value = mock_aggregation_pipeline - utils_mock.is_valid_move_result.return_value = False - database_mock.catalogue_categories.aggregate.return_value = MOCK_MOVE_QUERY_RESULT_VALID - - with pytest.raises(InvalidActionError) as exc: - catalogue_category_repository.update(catalogue_category.id, catalogue_category_in, session=session) - assert str(exc.value) == "Cannot move a catalogue category to one of its own children" - - utils_mock.create_move_check_aggregation_pipeline.assert_called_once_with( - entity_id=catalogue_category.id, destination_id=new_parent_id, collection_name="catalogue_categories" - ) - database_mock.catalogue_categories.aggregate.assert_called_once_with(mock_aggregation_pipeline, session=session) - utils_mock.is_valid_move_result.assert_called_once() - - database_mock.catalogue_categories.update_one.assert_not_called() - database_mock.catalogue_categories.find_one.assert_has_calls( - [ - call({"_id": CustomObjectId(new_parent_id)}, session=session), - call({"_id": CustomObjectId(catalogue_category.id)}, session=session), - call( - { - "parent_id": CustomObjectId(new_parent_id), - "code": catalogue_category.code, - "_id": {"$ne": CustomObjectId(catalogue_category.id)}, - }, - session=session, - ), - ] - ) + _has_child_elements_catalogue_category_id: str + _has_child_elements_result: bool + def call_has_child_elements(self, catalogue_category_id: str): + """Calls the CatalogueCategoryRepo `has_child_elements` method""" -def test_update_with_invalid_id(catalogue_category_repository): - """ - Test updating a catalogue category with invalid ID. + self._has_child_elements_catalogue_category_id = catalogue_category_id + self._has_child_elements_result = self.catalogue_category_repository.has_child_elements( + CustomObjectId(catalogue_category_id), session=self.mock_session + ) - Verify that the `update` method properly handles the update of a catalogue category with an invalid ID. - """ - update_catalogue_category = MagicMock() - catalogue_category_id = "invalid" + def check_has_child_elements_success(self, expected_result: bool): + """Checks that a prior call to `call_has_child_elements` worked as expected - with pytest.raises(InvalidObjectIdError) as exc: - catalogue_category_repository.update(catalogue_category_id, update_catalogue_category) - assert str(exc.value) == f"Invalid ObjectId value '{catalogue_category_id}'" + :param expected_result: The expected result returned by `has_child_elements` + """ + self.check_has_child_elements_performed_expected_calls(self._has_child_elements_catalogue_category_id) -def test_update_with_non_existent_parent_id(test_helpers, database_mock, catalogue_category_repository): - """ - Test updating a catalogue category with non-existent parent ID. + assert self._has_child_elements_result == expected_result - Verify that the `update` method properly handles the update of a catalogue category with non-existent parent ID. - """ - # pylint: disable=duplicate-code - update_catalogue_category = CatalogueCategoryIn( - name="Category A", - code="category-a", - is_leaf=False, - parent_id=str(ObjectId()), - properties=[], - ) - # pylint: enable=duplicate-code - # Mock `find_one` to not return a parent catalogue category document - test_helpers.mock_find_one(database_mock.catalogue_categories, None) +class TestHasChildElements(HasChildElementsDSL): + """Tests for `has_child_elements`""" - with pytest.raises(MissingRecordError) as exc: - catalogue_category_repository.update(str(ObjectId()), update_catalogue_category) - assert str(exc.value) == f"No parent catalogue category found with ID: {update_catalogue_category.parent_id}" + def test_has_child_elements_with_no_children(self): + """Test `has_child_elements` when there are no child catalogue categories or catalogue items""" + self.mock_has_child_elements(child_catalogue_category_data=None, child_catalogue_item_data=None) + self.call_has_child_elements(catalogue_category_id=str(ObjectId())) + self.check_has_child_elements_success(expected_result=False) -def test_update_duplicate_name_within_parent(test_helpers, database_mock, catalogue_category_repository): - """ - Test updating a catalogue category with a duplicate name within the parent catalogue category. + def test_has_child_elements_with_child_catalogue_category(self): + """Test `has_child_elements` when there is a child catalogue category but no child catalogue items""" - Verify that the `update` method properly handles the update of a catalogue category with a duplicate name in a - parent catalogue category. - """ - # pylint: disable=duplicate-code - update_catalogue_category = CatalogueCategoryIn( - name="Category B", - code="category-B", - is_leaf=False, - parent_id=None, - properties=[], - **MOCK_CREATED_MODIFIED_TIME, - ) - # pylint: enable=duplicate-code + self.mock_has_child_elements( + child_catalogue_category_data=CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A, + child_catalogue_item_data=None, + ) + self.call_has_child_elements(catalogue_category_id=str(ObjectId())) + self.check_has_child_elements_success(expected_result=True) - catalogue_category_id = str(ObjectId()) - # Mock `find_one` to return a catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **CATALOGUE_CATEGORY_INFO, - **MOCK_CREATED_MODIFIED_TIME, - "_id": CustomObjectId(catalogue_category_id), - "is_leaf": update_catalogue_category.is_leaf, - "parent_id": update_catalogue_category.parent_id, - "properties": update_catalogue_category.properties, - }, - ) - # Mock `find_one` to return duplicate catalogue category found - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **CATALOGUE_CATEGORY_INFO, - **MOCK_CREATED_MODIFIED_TIME, - "_id": ObjectId(), - }, - ) - - with pytest.raises(DuplicateRecordError) as exc: - catalogue_category_repository.update(catalogue_category_id, update_catalogue_category) - assert str(exc.value) == "Duplicate catalogue category found within the parent catalogue category" - - -def test_update_duplicate_name_within_new_parent(test_helpers, database_mock, catalogue_category_repository): - """ - Test updating a catalogue category with a duplicate name within a new parent catalogue category. - - Verify that the `update` method properly handles the update of a catalogue category with a duplicate name in a new - parent catalogue category. - """ - update_catalogue_category = CatalogueCategoryIn( - name="Category A", - code="category-a", - is_leaf=True, - parent_id=str(ObjectId()), - properties=[], - **MOCK_CREATED_MODIFIED_TIME, - ) - - # Mock `find_one` to return a parent catalogue category document - # pylint: disable=duplicate-code - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - "_id": update_catalogue_category.parent_id, - "name": "Category B", - "code": "category-b", - "is_leaf": False, - "parent_id": None, - "properties": [], - **MOCK_CREATED_MODIFIED_TIME, - }, - ) - # pylint: enable=duplicate-code - catalogue_category_id = str(ObjectId()) - # Mock `find_one` to return a catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - "_id": CustomObjectId(catalogue_category_id), - "name": update_catalogue_category.name, - "code": update_catalogue_category.code, - "is_leaf": update_catalogue_category.is_leaf, - "parent_id": None, - "properties": update_catalogue_category.properties, - **MOCK_CREATED_MODIFIED_TIME, - }, - ) - # Mock `find_one` to return duplicate catalogue category found - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **CATALOGUE_CATEGORY_INFO, - **MOCK_CREATED_MODIFIED_TIME, - "_id": ObjectId(), - }, - ) - - with pytest.raises(DuplicateRecordError) as exc: - catalogue_category_repository.update(catalogue_category_id, update_catalogue_category) - assert str(exc.value) == "Duplicate catalogue category found within the parent catalogue category" - - -def test_update_change_capitalisation_of_name_within_parent(test_helpers, database_mock, catalogue_category_repository): - """ - Test updating a catalogue category when the code is the same and the capitalisation of the name has changed. - - Verify that the `update` method properly handles the catalogue category to be updated, checks that the catalogue - category does not have child elements, there is not a duplicate catalogue category, and updates the catalogue - category. - """ - # pylint: disable=duplicate-code - catalogue_category = CatalogueCategoryOut( - id=str(ObjectId()), - name="CaTeGoRy a", - code="category-a", - is_leaf=False, - parent_id=None, - properties=[], - **MOCK_CREATED_MODIFIED_TIME, - ) - session = MagicMock() - # pylint: enable=duplicate-code + def test_has_child_elements_with_child_catalogue_catalogue_item(self): + """Test `has_child_elements` when there are no child catalogue categories but there is a child catalogue item""" - # Mock `find_one` to return a catalogue category document - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **CATALOGUE_CATEGORY_INFO, - **MOCK_CREATED_MODIFIED_TIME, - "_id": CustomObjectId(catalogue_category.id), - "is_leaf": catalogue_category.is_leaf, - "parent_id": catalogue_category.parent_id, - "properties": catalogue_category.properties, - }, - ) - # Mock `find_one` to return None as a duplicate was not found - test_helpers.mock_find_one(database_mock.catalogue_categories, None) - # Mock `update_one` to return an object for the updated catalogue category document - test_helpers.mock_update_one(database_mock.catalogue_categories) - # pylint: disable=duplicate-code - # Mock `find_one` to return the updated catalogue category document - catalogue_category_in = CatalogueCategoryIn( - **MOCK_CREATED_MODIFIED_TIME, - name=catalogue_category.name, - code=catalogue_category.code, - is_leaf=catalogue_category.is_leaf, - parent_id=catalogue_category.parent_id, - properties=catalogue_category.properties, - ) - # pylint: enable=duplicate-code - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **catalogue_category_in.model_dump(by_alias=True), - "_id": CustomObjectId(catalogue_category.id), - }, - ) - - updated_catalogue_category = catalogue_category_repository.update( - catalogue_category.id, catalogue_category_in, session=session - ) - - database_mock.catalogue_categories.update_one.assert_called_once_with( - {"_id": CustomObjectId(catalogue_category.id)}, - { - "$set": { - **catalogue_category_in.model_dump(by_alias=True), - } - }, - session=session, - ) - database_mock.catalogue_categories.find_one.assert_has_calls( - [ - call({"_id": CustomObjectId(catalogue_category.id)}, session=session), - call( - { - "parent_id": catalogue_category.parent_id, - "code": catalogue_category.code, - "_id": {"$ne": CustomObjectId(catalogue_category.id)}, - }, - session=session, - ), - call({"_id": CustomObjectId(catalogue_category.id)}, session=session), - ] - ) - assert updated_catalogue_category == CatalogueCategoryOut( - id=catalogue_category.id, **catalogue_category_in.model_dump(by_alias=True) - ) + # pylint:disable=fixme + # TODO: Replace CATALOGUE_ITEM_A_INFO once item tests have been refactored + self.mock_has_child_elements( + child_catalogue_category_data=None, + child_catalogue_item_data=CATALOGUE_ITEM_A_INFO, + ) + self.call_has_child_elements(catalogue_category_id=str(ObjectId())) + self.check_has_child_elements_success(expected_result=True) -def test_has_child_elements_with_no_child_categories(test_helpers, database_mock, catalogue_category_repository): - """ - Test has_child_elements returns false when there are no child categories - """ - # Mock `find_one` to return no child catalogue category document - test_helpers.mock_find_one(database_mock.catalogue_items, None) - test_helpers.mock_find_one(database_mock.catalogue_categories, None) +class DeleteDSL(CatalogueCategoryRepoDSL): + """Base class for delete tests""" - result = catalogue_category_repository.has_child_elements(ObjectId()) + _delete_catalogue_category_id: str + _delete_exception: pytest.ExceptionInfo - assert not result + def mock_delete( + self, + deleted_count: int, + child_catalogue_category_data: Optional[dict] = None, + child_catalogue_item_data: Optional[dict] = None, + ): + """Mocks database methods appropriately to test the 'delete' repo method + :param deleted_count: Number of documents deleted successfully + :param child_catalogue_category_data: Dictionary containing a child catalogue category's data (or None) + :param child_item_data: Dictionary containing a child catalogue item's data (or None) + """ -def test_has_child_elements_with_child_categories(test_helpers, database_mock, catalogue_category_repository): - """ - Test has_child_elements returns true when there are child categories - """ + self.mock_has_child_elements(child_catalogue_category_data, child_catalogue_item_data) + RepositoryTestHelpers.mock_delete_one(self.catalogue_categories_collection, deleted_count) - catalogue_category_id = str(ObjectId()) + def call_delete(self, catalogue_category_id: str): + """Calls the CatalogueCategoryRepo `delete` method""" - # Mock find_one to return 1 (child catalogue categories found) - test_helpers.mock_find_one( - database_mock.catalogue_categories, - { - **CATALOGUE_CATEGORY_INFO, - "_id": CustomObjectId(str(ObjectId())), - "parent_id": catalogue_category_id, - }, - ) - # Mock find_one to return 0 (child catalogue items not found) - test_helpers.mock_find_one(database_mock.catalogue_items, None) + self._delete_catalogue_category_id = catalogue_category_id + self.catalogue_category_repository.delete(catalogue_category_id, session=self.mock_session) - result = catalogue_category_repository.has_child_elements(catalogue_category_id) + def call_delete_expecting_error(self, catalogue_category_id: str, error_type: type[BaseException]): + """Calls the CatalogueCategoryRepo `delete` method while expecting an error to be raised""" - assert result + self._delete_catalogue_category_id = catalogue_category_id + with pytest.raises(error_type) as exc: + self.catalogue_category_repository.delete(catalogue_category_id) + self._delete_exception = exc + def check_delete_success(self): + """Checks that a prior call to `call_delete` worked as expected""" -def test_has_child_elements_with_child_catalogue_items(test_helpers, database_mock, catalogue_category_repository): - """ - Test has_child_elements returns true when there are child catalogue items. - """ - catalogue_category_id = str(ObjectId()) + self.check_has_child_elements_performed_expected_calls(self._delete_catalogue_category_id) + self.catalogue_categories_collection.delete_one.assert_called_once_with( + {"_id": CustomObjectId(self._delete_catalogue_category_id)}, session=self.mock_session + ) + + def check_delete_failed_with_exception(self, message: str, expecting_delete_one_called: bool = False): + """Checks that a prior call to `call_delete_expecting_error` worked as expected, raising an exception + with the correct message""" + + if not expecting_delete_one_called: + self.catalogue_categories_collection.delete_one.assert_not_called() + else: + self.catalogue_categories_collection.delete_one.assert_called_once_with( + {"_id": CustomObjectId(self._delete_catalogue_category_id)}, session=None + ) + + assert str(self._delete_exception.value) == message + + +# pylint: disable=duplicate-code +class TestDelete(DeleteDSL): + """Tests for deleting a catalogue category""" + + def test_delete(self): + """Test deleting a catalogue category""" + + self.mock_delete(deleted_count=1) + self.call_delete(str(ObjectId())) + self.check_delete_success() - # Mock `find_one` to return no child catalogue category document - test_helpers.mock_find_one(database_mock.catalogue_categories, None) - # pylint: disable=duplicate-code - # Mock `find_one` to return the child catalogue item document - test_helpers.mock_find_one( - database_mock.catalogue_items, - { - **FULL_CATALOGUE_ITEM_A_INFO, - "_id": CustomObjectId(str(ObjectId())), - "catalogue_category_id": CustomObjectId(catalogue_category_id), - }, - ) # pylint: enable=duplicate-code - result = catalogue_category_repository.has_child_elements(catalogue_category_id) - assert result + def test_delete_with_child_catalogue_category(self): + """Test deleting a catalogue category when it has a child catalogue category""" + catalogue_category_id = str(ObjectId()) -@patch("inventory_management_system_api.repositories.catalogue_category.datetime") -def test_create_property(datetime_mock, test_helpers, database_mock, catalogue_category_repository): - """ - Test create_property performs the correct database update query - """ - session = MagicMock() - catalogue_category_id = str(ObjectId()) - property_in = CatalogueCategoryPropertyIn(**MOCK_CATALOGUE_CATEGORY_PROPERTY_A_INFO) + self.mock_delete(deleted_count=1, child_catalogue_category_data=CATALOGUE_CATEGORY_IN_DATA_NON_LEAF_NO_PARENT_A) + self.call_delete_expecting_error(catalogue_category_id, ChildElementsExistError) + self.check_delete_failed_with_exception( + f"Catalogue category with ID {catalogue_category_id} has child elements and cannot be deleted" + ) - # Mock 'update_one' - test_helpers.mock_update_one(database_mock.catalogue_categories) + def test_delete_with_child_catalogue_item(self): + """Test deleting a catalogue category when it has a child Catalogue Item""" - result = catalogue_category_repository.create_property(catalogue_category_id, property_in, session=session) + catalogue_category_id = str(ObjectId()) - database_mock.catalogue_categories.update_one.assert_called_once_with( - {"_id": CustomObjectId(catalogue_category_id)}, - { - "$push": {"properties": property_in.model_dump(by_alias=True)}, - "$set": {"modified_time": datetime_mock.now.return_value}, - }, - session=session, - ) - assert result == CatalogueCategoryPropertyOut(**property_in.model_dump(by_alias=True)) + # pylint:disable=fixme + # TODO: Replace CATALOGUE_ITEM_A_INFO once item tests have been refactored + self.mock_delete(deleted_count=1, child_catalogue_item_data=CATALOGUE_ITEM_A_INFO) + self.call_delete_expecting_error(catalogue_category_id, ChildElementsExistError) + self.check_delete_failed_with_exception( + f"Catalogue category with ID {catalogue_category_id} has child elements and cannot be deleted" + ) + def test_delete_non_existent_id(self): + """Test deleting a catalogue category with a non-existent id""" -def test_create_property_with_invalid_id(database_mock, catalogue_category_repository): - """ - Test create_property performs the correct database update query when given an invalid id - """ + catalogue_category_id = str(ObjectId()) - with pytest.raises(InvalidObjectIdError) as exc: - catalogue_category_repository.create_property( - "invalid", CatalogueCategoryPropertyIn(**MOCK_CATALOGUE_CATEGORY_PROPERTY_A_INFO) + self.mock_delete(deleted_count=0) + self.call_delete_expecting_error(catalogue_category_id, MissingRecordError) + self.check_delete_failed_with_exception( + f"No catalogue category found with ID: {catalogue_category_id}", expecting_delete_one_called=True ) - assert str(exc.value) == "Invalid ObjectId value 'invalid'" - database_mock.catalogue_categories.update_one.assert_not_called() - - -@patch("inventory_management_system_api.repositories.catalogue_category.datetime") -def test_update_property(datetime_mock, test_helpers, database_mock, catalogue_category_repository): - """ - Test update_property performs the correct database update query - """ - session = MagicMock() - catalogue_category_id = str(ObjectId()) - property_id = str(ObjectId()) - property_in = CatalogueCategoryPropertyIn(**MOCK_CATALOGUE_CATEGORY_PROPERTY_A_INFO) - - # Mock 'update_one' - test_helpers.mock_update_one(database_mock.catalogue_categories) - - result = catalogue_category_repository.update_property( - catalogue_category_id, property_id, property_in, session=session - ) - - database_mock.catalogue_categories.update_one.assert_called_once_with( - { - "_id": CustomObjectId(catalogue_category_id), - "properties._id": CustomObjectId(property_id), - }, - { - "$set": { - "properties.$[elem]": property_in.model_dump(by_alias=True), - "modified_time": datetime_mock.now.return_value, + + def test_delete_invalid_id(self): + """Test deleting a catalogue category with an invalid id""" + + catalogue_category_id = "invalid-id" + + self.call_delete_expecting_error(catalogue_category_id, InvalidObjectIdError) + self.check_delete_failed_with_exception("Invalid ObjectId value 'invalid-id'") + + +class CreatePropertyDSL(CatalogueCategoryRepoDSL): + """Base class for create property tests""" + + _mock_datetime: Mock + _property_in: CatalogueCategoryPropertyIn + _expected_property_out: CatalogueCategoryPropertyOut + _created_property: CatalogueCategoryOut + _catalogue_category_id: str + _create_exception: pytest.ExceptionInfo + + @pytest.fixture(autouse=True) + def setup_create_property_dsl(self): + """Setup fixtures""" + + with patch("inventory_management_system_api.repositories.catalogue_category.datetime") as mock_datetime: + self._mock_datetime = mock_datetime + yield + + def mock_create_property(self, property_in_data: dict): + """Mocks database methods appropriately to test the 'create_property' repo method + + :param property_in_data: Dictionary containing the catalogue category property data as would be required for a + CatalogueCategoryPropertyIn database model + """ + + self._property_in = CatalogueCategoryPropertyIn(**property_in_data) + self._expected_property_out = CatalogueCategoryPropertyOut(**self._property_in.model_dump(by_alias=True)) + + RepositoryTestHelpers.mock_update_one(self.catalogue_categories_collection) + + def call_create_property(self, catalogue_category_id: str): + """Calls the CatalogueCategoryRepo `create_property` method with the appropriate data from a prior call to + `mock_create_property` + """ + + self._catalogue_category_id = catalogue_category_id + self._created_property = self.catalogue_category_repository.create_property( + catalogue_category_id, self._property_in, session=self.mock_session + ) + + def call_create_property_expecting_error(self, catalogue_category_id: str, error_type: type[BaseException]): + """Calls the CatalogueCategoryRepo `create_property` method with the appropriate data from a prior call to + `mock_create_property` + """ + + self._catalogue_category_id = catalogue_category_id + with pytest.raises(error_type) as exc: + self.catalogue_category_repository.create_property( + catalogue_category_id, self._property_in, session=self.mock_session + ) + self._create_exception = exc + + def check_create_property_success(self): + """Checks that a prior call to `call_create_property` worked as expected""" + + self.catalogue_categories_collection.update_one.assert_called_once_with( + {"_id": CustomObjectId(self._catalogue_category_id)}, + { + "$push": {"properties": self._property_in.model_dump(by_alias=True)}, + "$set": {"modified_time": self._mock_datetime.now.return_value}, }, - }, - array_filters=[{"elem._id": CustomObjectId(property_id)}], - session=session, - ) - assert result == CatalogueCategoryPropertyOut(**property_in.model_dump(by_alias=True)) - - -def test_update_property_with_invalid_catalogue_category_id(database_mock, catalogue_category_repository): - """ - Test update_property performs the correct database update query when given an invalid catalogue - category id - """ - - with pytest.raises(InvalidObjectIdError) as exc: - catalogue_category_repository.update_property( - "invalid", str(ObjectId()), CatalogueCategoryPropertyIn(**MOCK_CATALOGUE_CATEGORY_PROPERTY_A_INFO) + session=self.mock_session, ) - assert str(exc.value) == "Invalid ObjectId value 'invalid'" - database_mock.catalogue_categories.update_one.assert_not_called() + assert self._created_property == self._expected_property_out + + def check_create_property_failed_with_exception(self, message: str): + """Checks that a prior call to `call_create_property_expecting_error` worked as expected, raising an exception + with the correct message""" + + self.catalogue_categories_collection.update_one.assert_not_called() + + assert str(self._create_exception.value) == message -def test_update_property_with_invalid_property_id(database_mock, catalogue_category_repository): - """ - Test update_property performs the correct database update query when given an invalid property ID - """ +class TestCreateProperty(CreatePropertyDSL): + """Tests for creating a property""" + + def test_create_property(self): + """Test creating a property in an existing catalogue category""" + + self.mock_create_property(CATALOGUE_CATEGORY_PROPERTY_NUMBER_NON_MANDATORY_WITH_UNIT) + self.call_create_property(str(ObjectId())) + self.check_create_property_success() + + def test_create_property_with_invalid_id(self): + """Test creating a property in a catalogue category with an invalid id""" + + self.mock_create_property(CATALOGUE_CATEGORY_PROPERTY_NUMBER_NON_MANDATORY_WITH_UNIT) + self.call_create_property_expecting_error("invalid-id", InvalidObjectIdError) + self.check_create_property_failed_with_exception("Invalid ObjectId value 'invalid-id'") + + +class UpdatePropertyDSL(CreatePropertyDSL): + """Base class for update property tests""" + + _updated_property: CatalogueCategoryOut + _property_id: str + _update_exception: pytest.ExceptionInfo + + def mock_update_property(self, property_in_data: dict): + """Mocks database methods appropriately to test the `update_property` repo method + + :param property_in_data: Dictionary containing the catalogue category property data as would be required for a + CatalogueCategoryPropertyIn database model + """ + + self._property_in = CatalogueCategoryPropertyIn(**property_in_data) + self._expected_property_out = CatalogueCategoryPropertyOut(**self._property_in.model_dump(by_alias=True)) + + RepositoryTestHelpers.mock_update_one(self.catalogue_categories_collection) + + def call_update_property(self, catalogue_category_id: str, property_id: str): + """Calls the CatalogueCategoryRepo `update_property` method with the appropriate data from a prior call to + `mock_update_property` + """ + + self._catalogue_category_id = catalogue_category_id + self._property_id = property_id + self._updated_property = self.catalogue_category_repository.update_property( + catalogue_category_id, property_id, self._property_in, session=self.mock_session + ) + + def call_update_property_expecting_error( + self, catalogue_category_id: str, property_id: str, error_type: type[BaseException] + ): + """Calls the CatalogueCategoryRepo `update_property` method with the appropriate data from a prior call to + `mock_update_property` + """ + + self._catalogue_category_id = catalogue_category_id + self._property_id = property_id + with pytest.raises(error_type) as exc: + self.catalogue_category_repository.update_property( + catalogue_category_id, property_id, self._property_in, session=self.mock_session + ) + self._update_exception = exc + + def check_update_property_success(self): + """Checks that a prior call to `call_update_property` worked as expected""" + + self.catalogue_categories_collection.update_one.assert_called_once_with( + { + "_id": CustomObjectId(self._catalogue_category_id), + "properties._id": CustomObjectId(self._property_id), + }, + { + "$set": { + "properties.$[elem]": self._property_in.model_dump(by_alias=True), + "modified_time": self._mock_datetime.now.return_value, + }, + }, + array_filters=[{"elem._id": CustomObjectId(self._property_id)}], + session=self.mock_session, + ) + assert self._updated_property == self._expected_property_out + + def check_update_property_failed_with_exception(self, message: str): + """Checks that a prior call to `call_update_property_expecting_error` worked as expected, raising an exception + with the correct message""" + + self.catalogue_categories_collection.update_one.assert_not_called() + + assert str(self._update_exception.value) == message + + +class TestUpdateProperty(UpdatePropertyDSL): + """Tests for updating a property""" + + def test_update_property(self): + """Test updating a property in an existing catalogue category""" + + self.mock_update_property(CATALOGUE_CATEGORY_PROPERTY_NUMBER_NON_MANDATORY_WITH_UNIT) + self.call_update_property(catalogue_category_id=str(ObjectId()), property_id=str(ObjectId())) + self.check_update_property_success() + + def test_update_property_with_invalid_catalogue_category_id(self): + """Test updating a property in a catalogue category with an invalid id""" + + self.mock_update_property(CATALOGUE_CATEGORY_PROPERTY_NUMBER_NON_MANDATORY_WITH_UNIT) + self.call_update_property_expecting_error( + catalogue_category_id="invalid-id", property_id=str(ObjectId()), error_type=InvalidObjectIdError + ) + self.check_update_property_failed_with_exception("Invalid ObjectId value 'invalid-id'") + + def test_update_property_with_invalid_property_id(self): + """Test updating a property with an invalid id in a catalogue category""" - with pytest.raises(InvalidObjectIdError) as exc: - catalogue_category_repository.update_property( - str(ObjectId()), "invalid", CatalogueCategoryPropertyIn(**MOCK_CATALOGUE_CATEGORY_PROPERTY_A_INFO) + self.mock_update_property(CATALOGUE_CATEGORY_PROPERTY_NUMBER_NON_MANDATORY_WITH_UNIT) + self.call_update_property_expecting_error( + catalogue_category_id=str(ObjectId()), property_id="invalid-id", error_type=InvalidObjectIdError ) - assert str(exc.value) == "Invalid ObjectId value 'invalid'" - database_mock.catalogue_categories.update_one.assert_not_called() + self.check_update_property_failed_with_exception("Invalid ObjectId value 'invalid-id'") diff --git a/test/unit/repositories/test_system.py b/test/unit/repositories/test_system.py index 0fd1c117..478204be 100644 --- a/test/unit/repositories/test_system.py +++ b/test/unit/repositories/test_system.py @@ -31,7 +31,7 @@ class SystemRepoDSL: """Base class for SystemRepo unit tests""" - test_helpers: RepositoryTestHelpers + # pylint:disable=too-many-instance-attributes mock_database: Mock mock_utils: Mock system_repository: SystemRepo @@ -40,11 +40,14 @@ class SystemRepoDSL: mock_session = MagicMock() + # Internal data for utility functions + _mock_child_system_data: Optional[dict] + _mock_child_item_data: Optional[dict] + @pytest.fixture(autouse=True) - def setup(self, test_helpers, database_mock): + def setup(self, database_mock): """Setup fixtures""" - self.test_helpers = test_helpers self.mock_database = database_mock self.system_repository = SystemRepo(database_mock) self.systems_collection = database_mock.systems @@ -59,6 +62,67 @@ def setup(self, test_helpers, database_mock): self.mock_utils = mock_utils yield + def mock_has_child_elements(self, child_system_data: Optional[dict] = None, child_item_data: Optional[dict] = None): + """Mocks database methods appropriately for when the '_has_child_elements' repo method will be called + + :param child_system_data: Dictionary containing a child system's data (or None) + :param child_catalogue_item_data: Dictionary containing a child Item's data (or None) + """ + + self._mock_child_system_data = child_system_data + self._mock_child_item_data = child_item_data + + RepositoryTestHelpers.mock_find_one(self.systems_collection, child_system_data) + RepositoryTestHelpers.mock_find_one(self.items_collection, child_item_data) + + def check_has_child_elements_performed_expected_calls(self, expected_system_id: str): + """Checks that a call to `_has_child_elements` performed the expected function calls + + :param expected_system_id: Expected system id used in the database calls + """ + + self.systems_collection.find_one.assert_called_once_with( + {"parent_id": CustomObjectId(expected_system_id)}, session=self.mock_session + ) + # Will only call the second one if the first doesn't return anything + if not self._mock_child_item_data: + self.items_collection.find_one.assert_called_once_with( + {"system_id": CustomObjectId(expected_system_id)}, session=self.mock_session + ) + + def mock_is_duplicate_system(self, duplicate_system_in_data: Optional[dict] = None): + """Mocks database methods appropriately for when the '_is_duplicate_system' repo method will be called + + :param duplicate_system_in_data: Either None or a dictionary containing system data for a duplicate system + """ + + RepositoryTestHelpers.mock_find_one( + self.systems_collection, + ( + {**SystemIn(**duplicate_system_in_data).model_dump(), "_id": ObjectId()} + if duplicate_system_in_data + else None + ), + ) + + def get_is_duplicate_system_expected_find_one_call( + self, system_in: SystemIn, expected_system_id: Optional[CustomObjectId] + ): + """Returns the expected find_one calls from that should occur when `_is_duplicate_system` is called + + :param system_in: SystemIn model containing the data about the system + :param expected_system_id: Expected system_id provided to `is_duplicate_system` + """ + + return call( + { + "parent_id": system_in.parent_id, + "code": system_in.code, + "_id": {"$ne": expected_system_id}, + }, + session=self.mock_session, + ) + class CreateDSL(SystemRepoDSL): """Base class for create tests""" @@ -84,7 +148,7 @@ def mock_create( """ inserted_system_id = CustomObjectId(str(ObjectId())) - # Pass through system_in first as need creation and modified times + # Pass through SystemIn first as need creation and modified times self._system_in = SystemIn(**system_in_data) self._expected_system_out = SystemOut(**self._system_in.model_dump(), id=inserted_system_id) @@ -93,7 +157,7 @@ def mock_create( if system_in_data["parent_id"]: # If parent_system_data is given as None, then it is intentionally supposed to be, otherwise # pass through SystemIn first to ensure it has creation and modified times - self.test_helpers.mock_find_one( + RepositoryTestHelpers.mock_find_one( self.systems_collection, ( {**SystemIn(**parent_system_in_data).model_dump(), "_id": system_in_data["parent_id"]} @@ -101,16 +165,9 @@ def mock_create( else None ), ) - self.test_helpers.mock_find_one( - self.systems_collection, - ( - {**SystemIn(**duplicate_system_in_data).model_dump(), "_id": ObjectId()} - if duplicate_system_in_data - else None - ), - ) - self.test_helpers.mock_insert_one(self.systems_collection, inserted_system_id) - self.test_helpers.mock_find_one( + self.mock_is_duplicate_system(duplicate_system_in_data) + RepositoryTestHelpers.mock_insert_one(self.systems_collection, inserted_system_id) + RepositoryTestHelpers.mock_find_one( self.systems_collection, {**self._system_in.model_dump(), "_id": inserted_system_id} ) @@ -138,16 +195,7 @@ def check_create_success(self): if self._system_in.parent_id: expected_find_one_calls.append(call({"_id": self._system_in.parent_id}, session=self.mock_session)) # Also need checks for duplicate and the final newly inserted system get - expected_find_one_calls.append( - call( - { - "parent_id": self._system_in.parent_id, - "code": self._system_in.code, - "_id": {"$ne": None}, - }, - session=self.mock_session, - ) - ) + expected_find_one_calls.append(self.get_is_duplicate_system_expected_find_one_call(self._system_in, None)) expected_find_one_calls.append( call( {"_id": CustomObjectId(self._expected_system_out.id)}, @@ -169,17 +217,17 @@ def check_create_failed_with_exception(self, message: str): class TestCreate(CreateDSL): - """Tests for creating a System""" + """Tests for creating a system""" def test_create(self): - """Test creating a System""" + """Test creating a system""" self.mock_create(SYSTEM_IN_DATA_NO_PARENT_A) self.call_create() self.check_create_success() def test_create_with_parent_id(self): - """Test creating a System with a valid parent_id""" + """Test creating a system with a valid parent_id""" self.mock_create( {**SYSTEM_IN_DATA_NO_PARENT_A, "parent_id": str(ObjectId())}, @@ -189,24 +237,24 @@ def test_create_with_parent_id(self): self.check_create_success() def test_create_with_non_existent_parent_id(self): - """Test creating a System with a non-existent parent_id""" + """Test creating a system with a non-existent parent_id""" parent_id = str(ObjectId()) self.mock_create({**SYSTEM_IN_DATA_NO_PARENT_A, "parent_id": parent_id}, parent_system_in_data=None) self.call_create_expecting_error(MissingRecordError) - self.check_create_failed_with_exception(f"No parent System found with ID: {parent_id}") + self.check_create_failed_with_exception(f"No parent system found with ID: {parent_id}") def test_create_with_duplicate_name_within_parent(self): - """Test creating a System with a duplicate system being found in the same parent system""" + """Test creating a system with a duplicate system being found in the same parent system""" self.mock_create( {**SYSTEM_IN_DATA_NO_PARENT_A, "parent_id": str(ObjectId())}, parent_system_in_data=SYSTEM_IN_DATA_NO_PARENT_B, - duplicate_system_in_data=SYSTEM_IN_DATA_NO_PARENT_B, + duplicate_system_in_data=SYSTEM_IN_DATA_NO_PARENT_A, ) self.call_create_expecting_error(DuplicateRecordError) - self.check_create_failed_with_exception("Duplicate System found within the parent System") + self.check_create_failed_with_exception("Duplicate system found within the parent system") class GetDSL(SystemRepoDSL): @@ -221,8 +269,8 @@ def mock_get(self, system_id: str, system_in_data: Optional[dict]): """Mocks database methods appropriately to test the 'get' repo method :param system_id: ID of the system that will be obtained - :param system_in_data: Dictionary containing the system data as would be required for a SystemIn database - model (i.e. No id or created and modified times required) + :param system_in_data: Either None or a dictionary containing the system data as would be required for a + SystemIn database model (i.e. No id or created and modified times required) """ self._expected_system_out = ( @@ -231,7 +279,7 @@ def mock_get(self, system_id: str, system_in_data: Optional[dict]): else None ) - self.test_helpers.mock_find_one( + RepositoryTestHelpers.mock_find_one( self.systems_collection, self._expected_system_out.model_dump() if self._expected_system_out else None ) @@ -250,7 +298,7 @@ def call_get_expecting_error(self, system_id: str, error_type: type[BaseExceptio self._get_exception = exc def check_get_success(self): - """Checks that a prior call to `get_system` worked as expected""" + """Checks that a prior call to `call_get` worked as expected""" self.systems_collection.find_one.assert_called_once_with( {"_id": CustomObjectId(self._obtained_system_id)}, session=self.mock_session @@ -258,7 +306,7 @@ def check_get_success(self): assert self._obtained_system == self._expected_system_out def check_get_failed_with_exception(self, message: str): - """Checks that a prior call to `call_create_expecting_error` worked as expected, raising an exception + """Checks that a prior call to `call_get_expecting_error` worked as expected, raising an exception with the correct message""" self.systems_collection.find_one.assert_not_called() @@ -267,10 +315,10 @@ def check_get_failed_with_exception(self, message: str): class TestGet(GetDSL): - """Tests for getting a System""" + """Tests for getting a system""" def test_get(self): - """Test getting a System""" + """Test getting a system""" system_id = str(ObjectId()) @@ -279,7 +327,7 @@ def test_get(self): self.check_get_success() def test_get_with_non_existent_id(self): - """Test getting a System with a non-existent ID""" + """Test getting a system with a non-existent ID""" system_id = str(ObjectId()) @@ -288,7 +336,7 @@ def test_get_with_non_existent_id(self): self.check_get_success() def test_get_with_invalid_id(self): - """Test getting a System with an invalid ID""" + """Test getting a system with an invalid ID""" system_id = "invalid-id" @@ -342,10 +390,10 @@ def check_get_breadcrumbs_success(self): class TestGetBreadcrumbs(GetBreadcrumbsDSL): - """Tests for getting the breadcrumbs of a System""" + """Tests for getting the breadcrumbs of a system""" - def test_get_system_breadcrumbs(self): - """Test getting a System's breadcrumbs""" + def test_get_breadcrumbs(self): + """Test getting a system's breadcrumbs""" self.mock_breadcrumbs(MOCK_BREADCRUMBS_QUERY_RESULT_LESS_THAN_MAX_LENGTH) self.call_get_breadcrumbs(str(ObjectId())) @@ -363,13 +411,14 @@ def mock_list(self, systems_in_data: list[dict]): """Mocks database methods appropriately to test the 'list' repo method :param systems_in_data: List of dictionaries containing the system data as would be required for a - SystemIn database model (i.e. no id or created and modified times required)""" + SystemIn database model (i.e. no id or created and modified times required) + """ self._expected_systems_out = [ SystemOut(**SystemIn(**system_in_data).model_dump(), id=ObjectId()) for system_in_data in systems_in_data ] - self.test_helpers.mock_find( + RepositoryTestHelpers.mock_find( self.systems_collection, [system_out.model_dump() for system_out in self._expected_systems_out] ) @@ -392,31 +441,31 @@ def check_list_success(self): class TestList(ListDSL): - """Tests for listing System's""" + """Tests for listing system's""" def test_list(self): - """Test listing all Systems""" + """Test listing all systems""" self.mock_list([SYSTEM_IN_DATA_NO_PARENT_A, SYSTEM_IN_DATA_NO_PARENT_B]) self.call_list(parent_id=None) self.check_list_success() def test_list_with_parent_id_filter(self): - """Test listing all Systems with a given parent_id""" + """Test listing all systems with a given parent_id""" self.mock_list([SYSTEM_IN_DATA_NO_PARENT_A, SYSTEM_IN_DATA_NO_PARENT_B]) self.call_list(parent_id=str(ObjectId())) self.check_list_success() def test_list_with_null_parent_id_filter(self): - """Test listing all Systems with a 'null' parent_id""" + """Test listing all systems with a 'null' parent_id""" self.mock_list([SYSTEM_IN_DATA_NO_PARENT_A, SYSTEM_IN_DATA_NO_PARENT_B]) self.call_list(parent_id="null") self.check_list_success() def test_list_with_parent_id_with_no_results(self): - """Test listing all Systems with a parent_id filter returning no results""" + """Test listing all systems with a parent_id filter returning no results""" self.mock_list([]) self.call_list(parent_id=str(ObjectId())) @@ -428,18 +477,19 @@ class UpdateDSL(SystemRepoDSL): # pylint:disable=too-many-instance-attributes _system_in: SystemIn - _stored_system: Optional[SystemOut] + _stored_system_out: Optional[SystemOut] _expected_system_out: SystemOut _updated_system_id: str _updated_system: SystemOut _moving_system: bool _update_exception: pytest.ExceptionInfo - def set_update_data(self, new_system_data: dict): - """Assigns the update data to use during a call to `update_system` + def set_update_data(self, new_system_in_data: dict): + """Assigns the update data to use during a call to `call_update` - :param new_system_data: New system data to supply to the SystemRepo `update` method""" - self._system_in = SystemIn(**new_system_data) + :param new_system_data: New system data as would be required for a SystemIn database model to supply to the + SystemRepo `update` method""" + self._system_in = SystemIn(**new_system_in_data) # pylint:disable=too-many-arguments def mock_update( @@ -454,16 +504,16 @@ def mock_update( """Mocks database methods appropriately to test the 'update' repo method :param system_id: ID of the system that will be obtained - :param new_system_in_data: Dictionary containing the new system information as would be required for a SystemIn - database model (i.e. no id or created and modified times required) - :param stored_system_in_data: Dictionary containing the system information for the existing stored System + :param new_system_in_data: Dictionary containing the new system data as would be required for a SystemIn + database model (i.e. no id or created and modified times required) + :param stored_system_in_data: Dictionary containing the system data for the existing stored system as would be required for a SystemIn database model :param new_parent_system_in_data: Either None or a dictionary containing the new parent system data as would be - required for a SystemIn database model + required for a SystemIn database model :param duplicate_system_in_data: Either None or a dictionary containing the data for a duplicate system as would - be required for a SystemIn database model + be required for a SystemIn database model :param valid_move_result: Whether to mock in a valid or invalid move result i.e. when True will simulating - moving the system one of its own children + moving the system to one of its own children """ self.set_update_data(new_system_in_data) @@ -471,7 +521,7 @@ def mock_update( if new_system_in_data["parent_id"]: # If new_parent_system_data is given as none, then it is intentionally supposed to be, otherwise # pass through SystemIn first to ensure it has creation and modified times - self.test_helpers.mock_find_one( + RepositoryTestHelpers.mock_find_one( self.systems_collection, ( {**SystemIn(**new_parent_system_in_data).model_dump(), "_id": new_system_in_data["parent_id"]} @@ -481,32 +531,25 @@ def mock_update( ) # Stored system - self._stored_system = ( + self._stored_system_out = ( SystemOut(**SystemIn(**stored_system_in_data).model_dump(), id=CustomObjectId(system_id)) if stored_system_in_data else None ) - self.test_helpers.mock_find_one( - self.systems_collection, self._stored_system.model_dump() if self._stored_system else None + RepositoryTestHelpers.mock_find_one( + self.systems_collection, self._stored_system_out.model_dump() if self._stored_system_out else None ) # Duplicate check self._moving_system = stored_system_in_data is not None and ( new_system_in_data["parent_id"] != stored_system_in_data["parent_id"] ) - if (self._stored_system and (self._system_in.name != self._stored_system.name)) or self._moving_system: - self.test_helpers.mock_find_one( - self.systems_collection, - ( - {**SystemIn(**duplicate_system_in_data).model_dump(), "_id": ObjectId()} - if duplicate_system_in_data - else None - ), - ) + if (self._stored_system_out and (self._system_in.name != self._stored_system_out.name)) or self._moving_system: + self.mock_is_duplicate_system(duplicate_system_in_data) # Final system after update self._expected_system_out = SystemOut(**self._system_in.model_dump(), id=CustomObjectId(system_id)) - self.test_helpers.mock_find_one(self.systems_collection, self._expected_system_out.model_dump()) + RepositoryTestHelpers.mock_find_one(self.systems_collection, self._expected_system_out.model_dump()) if self._moving_system: mock_aggregation_pipeline = MagicMock() @@ -552,15 +595,10 @@ def check_update_success(self): ) # Duplicate check (which only runs if moving or changing the name) - if (self._stored_system and (self._system_in.name != self._stored_system.name)) or self._moving_system: + if (self._stored_system_out and (self._system_in.name != self._stored_system_out.name)) or self._moving_system: expected_find_one_calls.append( - call( - { - "parent_id": self._system_in.parent_id, - "code": self._system_in.code, - "_id": {"$ne": CustomObjectId(self._updated_system_id)}, - }, - session=self.mock_session, + self.get_is_duplicate_system_expected_find_one_call( + self._system_in, CustomObjectId(self._updated_system_id) ) ) self.systems_collection.find_one.assert_has_calls(expected_find_one_calls) @@ -599,10 +637,10 @@ def check_update_failed_with_exception(self, message: str): class TestUpdate(UpdateDSL): - """Tests for updating a System""" + """Tests for updating a system""" def test_update(self): - """Test updating a System""" + """Test updating a system""" system_id = str(ObjectId()) @@ -610,17 +648,8 @@ def test_update(self): self.call_update(system_id) self.check_update_success() - def test_update_with_invalid_id(self): - """Test updating a System with an invalid id""" - - system_id = "invalid-id" - - self.set_update_data(SYSTEM_IN_DATA_NO_PARENT_A) - self.call_update_expecting_error(system_id, InvalidObjectIdError) - self.check_update_failed_with_exception("Invalid ObjectId value 'invalid-id'") - def test_update_no_changes(self): - """Test updating a System to have exactly the same contents""" + """Test updating a system to have exactly the same contents""" system_id = str(ObjectId()) @@ -629,7 +658,7 @@ def test_update_no_changes(self): self.check_update_success() def test_update_parent_id(self): - """Test updating a System's parent_id to move it""" + """Test updating a system's parent_id to move it""" system_id = str(ObjectId()) @@ -645,7 +674,8 @@ def test_update_parent_id(self): self.check_update_success() def test_update_parent_id_to_child_of_self(self): - """Test updating a System's parent_id to a child of it self (should prevent this)""" + """Test updating a system's parent_id to a child of it self (should prevent this)""" + system_id = str(ObjectId()) self.mock_update( @@ -660,7 +690,7 @@ def test_update_parent_id_to_child_of_self(self): self.check_update_failed_with_exception("Cannot move a system to one of its own children") def test_update_with_non_existent_parent_id(self): - """Test updating a System's parent_id to a non-existent System""" + """Test updating a system's parent_id to a non-existent system""" system_id = str(ObjectId()) new_parent_id = str(ObjectId()) @@ -672,10 +702,10 @@ def test_update_with_non_existent_parent_id(self): new_parent_system_in_data=None, ) self.call_update_expecting_error(system_id, MissingRecordError) - self.check_update_failed_with_exception(f"No parent System found with ID: {new_parent_id}") + self.check_update_failed_with_exception(f"No parent system found with ID: {new_parent_id}") def test_update_name_to_duplicate_within_parent(self): - """Test updating a System's name to one that is a duplicate within the same parent System""" + """Test updating a system's name to one that is a duplicate within the same parent system""" system_id = str(ObjectId()) new_name = "New Duplicate Name" @@ -687,11 +717,11 @@ def test_update_name_to_duplicate_within_parent(self): duplicate_system_in_data=SYSTEM_IN_DATA_NO_PARENT_A, ) self.call_update_expecting_error(system_id, DuplicateRecordError) - self.check_update_failed_with_exception("Duplicate System found within the parent System") + self.check_update_failed_with_exception("Duplicate system found within the parent system") def test_update_parent_id_with_duplicate_within_parent(self): - """Test updating a System's parent-id to one contains a System with a duplicate name within the same parent - System""" + """Test updating a system's parent-id to one contains a system with a duplicate name within the same parent + system""" system_id = str(ObjectId()) new_parent_id = str(ObjectId()) @@ -704,7 +734,16 @@ def test_update_parent_id_with_duplicate_within_parent(self): duplicate_system_in_data=SYSTEM_IN_DATA_NO_PARENT_A, ) self.call_update_expecting_error(system_id, DuplicateRecordError) - self.check_update_failed_with_exception("Duplicate System found within the parent System") + self.check_update_failed_with_exception("Duplicate system found within the parent system") + + def test_update_with_invalid_id(self): + """Test updating a system with an invalid id""" + + system_id = "invalid-id" + + self.set_update_data(SYSTEM_IN_DATA_NO_PARENT_A) + self.call_update_expecting_error(system_id, InvalidObjectIdError) + self.check_update_failed_with_exception("Invalid ObjectId value 'invalid-id'") class DeleteDSL(SystemRepoDSL): @@ -720,12 +759,11 @@ def mock_delete( :param deleted_count: Number of documents deleted successfully :param child_system_data: Dictionary containing a child system's data (or None) - :param child_item_data: Dictionary containing a child items's data (or None) + :param child_item_data: Dictionary containing a child item's data (or None) """ - self.test_helpers.mock_find_one(self.systems_collection, child_system_data) - self.test_helpers.mock_find_one(self.items_collection, child_item_data) - self.test_helpers.mock_delete_one(self.systems_collection, deleted_count) + self.mock_has_child_elements(child_system_data, child_item_data) + RepositoryTestHelpers.mock_delete_one(self.systems_collection, deleted_count) def call_delete(self, system_id: str): """Calls the SystemRepo `delete` method""" @@ -744,12 +782,7 @@ def call_delete_expecting_error(self, system_id: str, error_type: type[BaseExcep def check_delete_success(self): """Checks that a prior call to `call_delete` worked as expected""" - self.systems_collection.find_one.assert_called_once_with( - {"parent_id": CustomObjectId(self._delete_system_id)}, session=self.mock_session - ) - self.items_collection.find_one.assert_called_once_with( - {"system_id": CustomObjectId(self._delete_system_id)}, session=self.mock_session - ) + self.check_has_child_elements_performed_expected_calls(self._delete_system_id) self.systems_collection.delete_one.assert_called_once_with( {"_id": CustomObjectId(self._delete_system_id)}, session=self.mock_session ) @@ -769,17 +802,17 @@ def check_delete_failed_with_exception(self, message: str, expecting_delete_one_ class TestDelete(DeleteDSL): - """Tests for deleting a System""" + """Tests for deleting a system""" def test_delete(self): - """Test deleting a System""" + """Test deleting a system""" self.mock_delete(deleted_count=1) self.call_delete(str(ObjectId())) self.check_delete_success() def test_delete_with_child_system(self): - """Test deleting a System when it has a child system""" + """Test deleting a system when it has a child system""" system_id = str(ObjectId()) @@ -788,27 +821,29 @@ def test_delete_with_child_system(self): self.check_delete_failed_with_exception(f"System with ID {system_id} has child elements and cannot be deleted") def test_delete_with_child_item(self): - """Test deleting a System when it has a child item""" + """Test deleting a system when it has a child Item""" system_id = str(ObjectId()) + # pylint:disable=fixme + # TODO: Replace ITEM_INFO once item tests have been refactored self.mock_delete(deleted_count=1, child_item_data=ITEM_INFO) self.call_delete_expecting_error(system_id, ChildElementsExistError) self.check_delete_failed_with_exception(f"System with ID {system_id} has child elements and cannot be deleted") def test_delete_non_existent_id(self): - """Test deleting a System""" + """Test deleting a system with a non-existent id""" system_id = str(ObjectId()) self.mock_delete(deleted_count=0) self.call_delete_expecting_error(system_id, MissingRecordError) self.check_delete_failed_with_exception( - f"No System found with ID: {system_id}", expecting_delete_one_called=True + f"No system found with ID: {system_id}", expecting_delete_one_called=True ) def test_delete_invalid_id(self): - """Test deleting a System""" + """Test deleting a system with an invalid id""" system_id = "invalid-id" diff --git a/test/unit/repositories/test_unit.py b/test/unit/repositories/test_unit.py index 624d223a..4d2e572d 100644 --- a/test/unit/repositories/test_unit.py +++ b/test/unit/repositories/test_unit.py @@ -3,7 +3,6 @@ """ from test.unit.repositories.mock_models import MOCK_CREATED_MODIFIED_TIME -from test.unit.repositories.test_catalogue_category import CATALOGUE_CATEGORY_INFO from unittest.mock import MagicMock, call import pytest @@ -18,6 +17,16 @@ ) from inventory_management_system_api.models.unit import UnitIn, UnitOut +# pylint: disable=duplicate-code +CATALOGUE_CATEGORY_INFO = { + "name": "Category A", + "code": "category-a", + "is_leaf": False, + "parent_id": None, + "properties": [], +} +# pylint: enable=duplicate-code + def test_create(test_helpers, database_mock, unit_repository): """ diff --git a/test/unit/services/conftest.py b/test/unit/services/conftest.py index ce9de64e..d42197ee 100644 --- a/test/unit/services/conftest.py +++ b/test/unit/services/conftest.py @@ -321,6 +321,8 @@ def mock_update( repository_mock.update.return_value = repo_obj +# pylint:disable=fixme +# TODO: Remove this once tests refactored - should be able to just use `ServiceTestHelpers.` @pytest.fixture(name="test_helpers") def fixture_test_helpers() -> Type[ServiceTestHelpers]: """ diff --git a/test/unit/services/test_catalogue_category.py b/test/unit/services/test_catalogue_category.py index aaf11ea3..b169bcd3 100644 --- a/test/unit/services/test_catalogue_category.py +++ b/test/unit/services/test_catalogue_category.py @@ -475,7 +475,7 @@ def test_get_breadcrumbs(test_helpers, catalogue_category_repository_mock, catal """ Test getting breadcrumbs for a catalogue category - Verify that the `get_breadcrumbs` method properly handles the retrieval of a System + Verify that the `get_breadcrumbs` method properly handles the retrieval of breadcrumbs for a catalogue category """ catalogue_category_id = str(ObjectId()) breadcrumbs = MagicMock() diff --git a/test/unit/services/test_system.py b/test/unit/services/test_system.py index 694fd100..ebdc0bba 100644 --- a/test/unit/services/test_system.py +++ b/test/unit/services/test_system.py @@ -21,7 +21,6 @@ class SystemServiceDSL: """Base class for SystemService unit tests""" - test_helpers: ServiceTestHelpers wrapped_utils: Mock mock_system_repository: Mock system_service: SystemService @@ -29,7 +28,6 @@ class SystemServiceDSL: @pytest.fixture(autouse=True) def setup( self, - test_helpers, system_repository_mock, system_service, # Ensures all created and modified times are mocked throughout @@ -38,7 +36,6 @@ def setup( ): """Setup fixtures""" - self.test_helpers = test_helpers self.mock_system_repository = system_repository_mock self.system_service = system_service @@ -62,6 +59,7 @@ def mock_create(self, system_post_data: dict): :param system_post_data: Dictionary containing the basic system data as would be required for a SystemPostSchema (i.e. no id, code or created and modified times required) """ + self._system_post = SystemPostSchema(**system_post_data) self._expected_system_in = SystemIn( @@ -69,7 +67,7 @@ def mock_create(self, system_post_data: dict): ) self._expected_system_out = SystemOut(**self._expected_system_in.model_dump(), id=ObjectId()) - self.test_helpers.mock_create(self.mock_system_repository, self._expected_system_out) + ServiceTestHelpers.mock_create(self.mock_system_repository, self._expected_system_out) def call_create(self): """Calls the SystemService `create` method with the appropriate data from a prior call to `mock_create`""" @@ -86,17 +84,17 @@ def check_create_success(self): class TestCreate(CreateDSL): - """Tests for creating a System""" + """Tests for creating a system""" def test_create(self): - """Test creating a System""" + """Test creating a system""" self.mock_create(SYSTEM_POST_DATA_NO_PARENT_A) self.call_create() self.check_create_success() def test_create_with_parent_id(self): - """Test creating a System with a parent ID""" + """Test creating a system with a parent ID""" self.mock_create({**SYSTEM_POST_DATA_NO_PARENT_A, "parent_id": str(ObjectId())}) self.call_create() @@ -115,7 +113,7 @@ def mock_get(self): # Simply a return currently, so no need to use actual data self._expected_system = MagicMock() - self.test_helpers.mock_get(self.mock_system_repository, self._expected_system) + ServiceTestHelpers.mock_get(self.mock_system_repository, self._expected_system) def call_get(self, system_id: str): """Calls the SystemService `get` method""" @@ -132,10 +130,10 @@ def check_get_success(self): class TestGet(GetDSL): - """Tests for getting a System""" + """Tests for getting a system""" def test_get(self): - """Test getting a System""" + """Test getting a system""" self.mock_get() self.call_get(str(ObjectId())) @@ -154,7 +152,7 @@ def mock_get_breadcrumbs(self): # Simply a return currently, so no need to use actual data self._expected_breadcrumbs = MagicMock() - self.test_helpers.mock_get_breadcrumbs(self.mock_system_repository, self._expected_breadcrumbs) + ServiceTestHelpers.mock_get_breadcrumbs(self.mock_system_repository, self._expected_breadcrumbs) def call_get_breadcrumbs(self, system_id: str): """Calls the SystemService `get` method""" @@ -171,10 +169,10 @@ def check_get_breadcrumbs_success(self): class TestGetBreadcrumbs(GetBreadcrumbsDSL): - """Tests for getting the breadcrumbs of a System""" + """Tests for getting the breadcrumbs of a system""" def test_get_breadcrumbs(self): - """Test getting a System's breadcrumbs""" + """Test getting a system's breadcrumbs""" self.mock_get_breadcrumbs() self.call_get_breadcrumbs(str(ObjectId())) @@ -193,7 +191,7 @@ def mock_list(self): # Simply a return currently, so no need to use actual data self._expected_systems = MagicMock() - self.test_helpers.mock_list(self.mock_system_repository, self._expected_systems) + ServiceTestHelpers.mock_list(self.mock_system_repository, self._expected_systems) def call_list(self, parent_id: Optional[str]): """Calls the SystemService `list` method""" @@ -210,10 +208,10 @@ def check_list_success(self): class TestList(ListDSL): - """Tests for getting a System""" + """Tests for getting a system""" def test_list(self): - """Test listing Systems""" + """Test listing systems""" self.mock_list() self.call_list(str(ObjectId())) @@ -237,7 +235,7 @@ def mock_update(self, system_id: str, system_patch_data: dict, stored_system_pos :param system_id: ID of the system that will be obtained :param system_patch_data: Dictionary containing the patch data as would be required for a SystemPatchSchema (i.e. no id, code or created and modified times required) - :param stored_system_post_data: Dictionary containing the system data for the existing stored System + :param stored_system_post_data: Dictionary containing the system data for the existing stored system as would be required for a SystemPostSchema (i.e. no id, code or created and modified times required) """ @@ -253,14 +251,14 @@ def mock_update(self, system_id: str, system_patch_data: dict, stored_system_pos if stored_system_post_data else None ) - self.test_helpers.mock_get(self.mock_system_repository, self._stored_system) + ServiceTestHelpers.mock_get(self.mock_system_repository, self._stored_system) # Patch schema self._system_patch = SystemPatchSchema(**system_patch_data) # Updated system self._expected_system_out = MagicMock() - self.test_helpers.mock_update(self.mock_system_repository, self._expected_system_out) + ServiceTestHelpers.mock_update(self.mock_system_repository, self._expected_system_out) # Construct the expected input for the repository merged_system_data = {**(stored_system_post_data or {}), **system_patch_data} @@ -313,7 +311,7 @@ class TestUpdate(UpdateDSL): """Tests for updating a system""" def test_update_all_fields(self): - """Test updating all fields of a System""" + """Test updating all fields of a system""" system_id = str(ObjectId()) @@ -326,7 +324,7 @@ def test_update_all_fields(self): self.check_update_success() def test_update_description_field_only(self): - """Test updating System's description field only (code should not need regenerating as name doesn't change)""" + """Test updating system's description field only (code should not need regenerating as name doesn't change)""" system_id = str(ObjectId()) @@ -339,13 +337,13 @@ def test_update_description_field_only(self): self.check_update_success() def test_update_with_non_existent_id(self): - """Test updating a System with a non-existent ID""" + """Test updating a system with a non-existent ID""" system_id = str(ObjectId()) self.mock_update(system_id, system_patch_data=SYSTEM_POST_DATA_NO_PARENT_B, stored_system_post_data=None) self.call_update_expecting_error(system_id, MissingRecordError) - self.check_update_failed_with_exception(f"No System found with ID: {system_id}") + self.check_update_failed_with_exception(f"No system found with ID: {system_id}") class DeleteDSL(SystemServiceDSL): @@ -366,10 +364,10 @@ def check_delete_success(self): class TestDelete(DeleteDSL): - """Tests for deleting a System""" + """Tests for deleting a system""" def test_delete(self): - """Test deleting a System""" + """Test deleting a system""" self.call_delete(str(ObjectId())) self.check_delete_success()