From 14b6abb4670e4503ebade4a839d62888f6295719 Mon Sep 17 00:00:00 2001 From: jcalarcon98 Date: Fri, 18 Jun 2021 11:27:28 -0500 Subject: [PATCH] fix: TT-270 Fix handle exceptions in create update entries --- tests/conftest.py | 12 +- .../time_entries/time_entries_model_test.py | 28 ++++- .../time_entries_namespace_test.py | 9 ++ tests/utils/validate_entries_test.py | 113 ++++++++++++++++++ .../time_entries/time_entries_namespace.py | 2 +- .../time_entries/time_entries_repository.py | 16 +++ utils/validate_entries.py | 58 +++++++++ 7 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 tests/utils/validate_entries_test.py create mode 100644 utils/validate_entries.py diff --git a/tests/conftest.py b/tests/conftest.py index ef24c3ea..02fefa38 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import copy from datetime import datetime, timedelta +from http import HTTPStatus import jwt import pytest @@ -217,13 +218,22 @@ def time_entries_dao(): return time_entries_dao -@pytest.fixture(scope="module") +@pytest.fixture def running_time_entry( time_entry_repository: TimeEntryCosmosDBRepository, owner_id: str, tenant_id: str, event_context: EventContext, + mocker, ): + mocker.patch( + 'time_tracker_api.time_entries.time_entries_repository.are_related_entry_entities_valid', + return_value={ + "is_valid": True, + "status_code": HTTPStatus.OK, + "message": "Related entry entities valid", + }, + ) current_time_entry_repository = copy.copy(time_entry_repository) created_time_entry = current_time_entry_repository.create( { diff --git a/tests/time_tracker_api/time_entries/time_entries_model_test.py b/tests/time_tracker_api/time_entries/time_entries_model_test.py index c4d8b354..fd04166e 100644 --- a/tests/time_tracker_api/time_entries/time_entries_model_test.py +++ b/tests/time_tracker_api/time_entries/time_entries_model_test.py @@ -1,11 +1,10 @@ +from http import HTTPStatus from unittest.mock import Mock, patch import pytest from faker import Faker +from commons.data_access_layer.cosmos_db import CustomError from commons.data_access_layer.database import EventContext -from time_tracker_api.time_entries.time_entries_model import ( - TimeEntryCosmosDBModel, -) from time_tracker_api.time_entries.time_entries_repository import ( TimeEntryCosmosDBRepository, TimeEntryCosmosDBModel, @@ -17,6 +16,7 @@ def create_time_entry( end_date: str, owner_id: str, tenant_id: str, + mocker, event_context: EventContext, time_entry_repository: TimeEntryCosmosDBRepository, ) -> TimeEntryCosmosDBModel: @@ -30,6 +30,15 @@ def create_time_entry( "tenant_id": tenant_id, } + mocker.patch( + 'time_tracker_api.time_entries.time_entries_repository.are_related_entry_entities_valid', + return_value={ + "is_valid": True, + "status_code": HTTPStatus.OK, + "message": "Related entry entities valid", + }, + ) + created_item = time_entry_repository.create( data, event_context, mapper=TimeEntryCosmosDBModel ) @@ -78,6 +87,7 @@ def test_find_interception_with_date_range_should_find( end_date_: str, owner_id: str, tenant_id: str, + mocker, time_entry_repository: TimeEntryCosmosDBRepository, event_context: EventContext, ): @@ -86,6 +96,7 @@ def test_find_interception_with_date_range_should_find( end_date, owner_id, tenant_id, + mocker, event_context, time_entry_repository, ) @@ -142,12 +153,14 @@ def test_find_interception_with_date_range_should_not_find( tenant_id: str, time_entry_repository: TimeEntryCosmosDBRepository, event_context: EventContext, + mocker, ): existing_item = create_time_entry( start_date, end_date, owner_id, tenant_id, + mocker, event_context, time_entry_repository, ) @@ -171,14 +184,17 @@ def test_find_interception_should_ignore_id_of_existing_item( tenant_id: str, time_entry_repository: TimeEntryCosmosDBRepository, event_context: EventContext, + mocker, ): start_date = "2020-10-01T05:00:00.000Z" end_date = "2020-10-01T10:00:00.000Z" + existing_item = create_time_entry( start_date, end_date, owner_id, tenant_id, + mocker, event_context, time_entry_repository, ) @@ -229,10 +245,10 @@ def test_find_running_should_not_find_any_item( owner_id: str, time_entry_repository: TimeEntryCosmosDBRepository, ): - try: + with pytest.raises(CustomError) as custom_error: time_entry_repository.find_running(tenant_id, owner_id) - except Exception as e: - assert type(e) is StopIteration + + assert custom_error.value.code == HTTPStatus.NO_CONTENT @patch( diff --git a/tests/time_tracker_api/time_entries/time_entries_namespace_test.py b/tests/time_tracker_api/time_entries/time_entries_namespace_test.py index 0954bd7c..8f22f45f 100644 --- a/tests/time_tracker_api/time_entries/time_entries_namespace_test.py +++ b/tests/time_tracker_api/time_entries/time_entries_namespace_test.py @@ -749,6 +749,15 @@ def test_create_with_valid_uuid_format_should_return_created( valid_uuid: str, time_entries_dao, ): + mocker.patch( + 'time_tracker_api.time_entries.time_entries_repository.are_related_entry_entities_valid', + return_value={ + "is_valid": True, + "status_code": HTTPStatus.OK, + "message": "Related entry entities valid", + }, + ) + repository_container_create_item_mock = mocker.patch.object( time_entries_dao.repository.container, 'create_item', diff --git a/tests/utils/validate_entries_test.py b/tests/utils/validate_entries_test.py new file mode 100644 index 00000000..5e8be2a4 --- /dev/null +++ b/tests/utils/validate_entries_test.py @@ -0,0 +1,113 @@ +from http import HTTPStatus + +from azure.cosmos.exceptions import CosmosResourceNotFoundError +from faker import Faker + +from time_tracker_api.activities import activities_model +from time_tracker_api.activities.activities_model import ActivityCosmosDBDao +from time_tracker_api.projects.projects_model import ProjectCosmosDBDao +from utils.validate_entries import ( + are_related_entry_entities_valid, + exists_related_entity, +) + +fake = Faker() + + +def test_validate_related_entry_entities_must_failed_if_project_id_is_empty(): + are_entities_valid = are_related_entry_entities_valid( + project_id=None, activity_id=fake.uuid4() + ) + + assert are_entities_valid.get('is_valid') is False + assert are_entities_valid.get('status_code') == HTTPStatus.BAD_REQUEST + assert are_entities_valid.get('message') == "Project id can not be empty" + + +def test_validate_related_entry_entities_must_failed_if_activity_id_is_empty(): + are_entities_valid = are_related_entry_entities_valid( + project_id=fake.uuid4(), activity_id=None + ) + + assert are_entities_valid.get('is_valid') is False + assert are_entities_valid.get('status_code') == HTTPStatus.BAD_REQUEST + assert are_entities_valid.get('message') == "Activity id can not be empty" + + +def test_validate_related_entry_entities_must_pass_if_the_data_is_valid( + mocker, +): + mocker.patch.object(ProjectCosmosDBDao, 'get') + mocker.patch.object(ActivityCosmosDBDao, 'get') + + are_entities_valid = are_related_entry_entities_valid( + project_id=fake.uuid4(), activity_id=fake.uuid4() + ) + + assert are_entities_valid.get('is_valid') is True + assert are_entities_valid.get('status_code') == HTTPStatus.OK + assert are_entities_valid.get('message') == 'Related entry entities valid' + + +def test_validate_related_entry_entities_must_fail_if_the_project_id_does_not_exists( + mocker, +): + mocker.patch( + 'utils.validate_entries.exists_related_entity', return_value=False + ) + + are_entities_valid = are_related_entry_entities_valid( + project_id=fake.uuid4(), activity_id=fake.uuid4() + ) + + assert are_entities_valid.get('is_valid') is False + assert are_entities_valid.get('status_code') == HTTPStatus.BAD_REQUEST + assert ( + are_entities_valid.get('message') == 'Related Project does not exists' + ) + + +def test_validate_related_entry_entities_must_fail_if_the_activity_id_does_not_exists( + mocker, +): + mocker.patch.object(ProjectCosmosDBDao, 'get') + + mocker.patch.object( + ActivityCosmosDBDao, 'get', side_effect=CosmosResourceNotFoundError + ) + + are_entities_valid = are_related_entry_entities_valid( + project_id=fake.uuid4(), activity_id=fake.uuid4() + ) + + assert are_entities_valid.get('is_valid') is False + assert are_entities_valid.get('status_code') == HTTPStatus.BAD_REQUEST + assert ( + are_entities_valid.get('message') == 'Related Activity does not exists' + ) + + +def test_exists_related_entity_should_return_true_if_entity_exists(mocker): + mocker.patch.object(ActivityCosmosDBDao, 'get') + activity_dao = activities_model.create_dao() + + exists_entity = exists_related_entity( + related_id=fake.uuid4(), dao=activity_dao + ) + + assert exists_entity is True + + +def test_exists_related_entity_should_return_false_if_entity_does_not_exists( + mocker, +): + mocker.patch.object( + ActivityCosmosDBDao, 'get', side_effect=CosmosResourceNotFoundError + ) + activity_dao = activities_model.create_dao() + + exists_entity = exists_related_entity( + related_id=fake.uuid4(), dao=activity_dao + ) + + assert exists_entity is False diff --git a/time_tracker_api/time_entries/time_entries_namespace.py b/time_tracker_api/time_entries/time_entries_namespace.py index 47a9a202..31a9f0af 100644 --- a/time_tracker_api/time_entries/time_entries_namespace.py +++ b/time_tracker_api/time_entries/time_entries_namespace.py @@ -37,7 +37,7 @@ ), 'activity_id': UUID( title='Activity', - required=False, + required=True, description='The id of the selected activity', example=faker.uuid4(), ), diff --git a/time_tracker_api/time_entries/time_entries_repository.py b/time_tracker_api/time_entries/time_entries_repository.py index a0deb2b3..5abed126 100644 --- a/time_tracker_api/time_entries/time_entries_repository.py +++ b/time_tracker_api/time_entries/time_entries_repository.py @@ -30,6 +30,7 @@ ) from utils.query_builder import CosmosDBQueryBuilder, Order from utils.time import str_to_datetime +from utils.validate_entries import are_related_entry_entities_valid class TimeEntryCosmosDBRepository(CosmosDBRepository): @@ -303,6 +304,21 @@ def find_running( def validate_data(self, data, event_context: EventContext): start_date = data.get('start_date') + related_project_id = data.get('project_id') + related_activity_id = data.get('activity_id') + + are_related_entities_valid = are_related_entry_entities_valid( + project_id=related_project_id, activity_id=related_activity_id + ) + + if not are_related_entities_valid.get('is_valid'): + status_code = are_related_entities_valid.get('status_code') + message = are_related_entities_valid.get('message') + raise CustomError( + status_code, + description=message, + ) + if data.get('end_date') is not None: if data['end_date'] <= start_date: raise CustomError( diff --git a/utils/validate_entries.py b/utils/validate_entries.py new file mode 100644 index 00000000..dd0dcbb4 --- /dev/null +++ b/utils/validate_entries.py @@ -0,0 +1,58 @@ +from http import HTTPStatus + +from azure.cosmos.exceptions import CosmosResourceNotFoundError + +from time_tracker_api.projects import projects_model +from time_tracker_api.activities import activities_model + + +def are_related_entry_entities_valid(project_id: str, activity_id: str): + if not project_id: + return { + "is_valid": False, + "status_code": HTTPStatus.BAD_REQUEST, + "message": "Project id can not be empty", + } + + if not activity_id: + return { + "is_valid": False, + "status_code": HTTPStatus.BAD_REQUEST, + "message": "Activity id can not be empty", + } + + exists_project = exists_related_entity( + project_id, projects_model.create_dao() + ) + + if not exists_project: + return { + "is_valid": False, + "status_code": HTTPStatus.BAD_REQUEST, + "message": "Related Project does not exists", + } + + exists_activity = exists_related_entity( + activity_id, activities_model.create_dao() + ) + + if not exists_activity: + return { + "is_valid": False, + "status_code": HTTPStatus.BAD_REQUEST, + "message": "Related Activity does not exists", + } + + return { + "is_valid": True, + "status_code": HTTPStatus.OK, + "message": "Related entry entities valid", + } + + +def exists_related_entity(related_id: str, dao): + try: + dao.get(related_id) + return True + except CosmosResourceNotFoundError: + return False