From 317a22ad63666de1048a9ef4ac3c7d529731fab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Soto?= Date: Tue, 26 Oct 2021 17:17:23 -0500 Subject: [PATCH 1/2] feat: TT-366 Add update activities method --- .gitignore | 4 -- V2/serverless.yml | 12 +++- .../azure/activity_azure_endpoints_test.py | 40 +++++++++--- .../daos/activities_json_dao_test.py | 65 ++++++++++++------- .../unit/services/activity_service_test.py | 17 +++++ .../use_cases/activities_use_case_test.py | 17 +++++ V2/time_entries/_application/__init__.py | 1 + .../_application/_activities/__init__.py | 1 + .../_activities/_update_activity.py | 44 +++++++++++++ V2/time_entries/_domain/__init__.py | 6 +- .../_persistence_contracts/_activities_dao.py | 4 ++ .../_domain/_services/_activity.py | 3 + .../_domain/_use_cases/__init__.py | 1 + .../_use_cases/_update_activity_use_case.py | 11 ++++ .../_data_persistence/_activities_json_dao.py | 26 ++++++++ V2/time_entries/interface.py | 1 + 16 files changed, 212 insertions(+), 41 deletions(-) create mode 100644 V2/time_entries/_application/_activities/_update_activity.py create mode 100644 V2/time_entries/_domain/_use_cases/_update_activity_use_case.py diff --git a/.gitignore b/.gitignore index f754bfdf..a9edb626 100644 --- a/.gitignore +++ b/.gitignore @@ -29,10 +29,6 @@ node_modules # Serverless directories .serverless/ -# Azure Functions json config -host.json -local.settings.json - # Files generated for development .env timetracker-api-postman-collection.json diff --git a/V2/serverless.yml b/V2/serverless.yml index 5c08f749..223c8a33 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -55,4 +55,14 @@ functions: methods: - DELETE route: activities/{id} - authLevel: anonymous \ No newline at end of file + authLevel: anonymous + + update_activity: + handler: time_entries/interface.update_activity + events: + - http: true + x-azure-settings: + methods: + - PUT + route: activities/{id} + authLevel: anonymous diff --git a/V2/tests/api/azure/activity_azure_endpoints_test.py b/V2/tests/api/azure/activity_azure_endpoints_test.py index 824a52d4..3eab39f1 100644 --- a/V2/tests/api/azure/activity_azure_endpoints_test.py +++ b/V2/tests/api/azure/activity_azure_endpoints_test.py @@ -1,7 +1,6 @@ -from time_entries._application._activities import ( - _get_activities, - _delete_activity, -) +from time_entries._application import _activities as activities +from faker import Faker + import azure.functions as func import json import typing @@ -11,10 +10,10 @@ def test__activity_azure_endpoint__returns_all_activities( create_temp_activities, ): activities_json, tmp_directory = create_temp_activities - _get_activities.JSON_PATH = tmp_directory + activities._get_activities.JSON_PATH = tmp_directory req = func.HttpRequest(method='GET', body=None, url='/api/activities') - response = _get_activities.get_activities(req) + response = activities.get_activities(req) activities_json_data = response.get_body().decode("utf-8") assert response.status_code == 200 @@ -25,7 +24,7 @@ def test__activity_azure_endpoint__returns_an_activity__when_activity_matches_it create_temp_activities, ): activities_json, tmp_directory = create_temp_activities - _get_activities.JSON_PATH = tmp_directory + activities._get_activities.JSON_PATH = tmp_directory req = func.HttpRequest( method='GET', body=None, @@ -33,7 +32,7 @@ def test__activity_azure_endpoint__returns_an_activity__when_activity_matches_it route_params={"id": activities_json[0]['id']}, ) - response = _get_activities.get_activities(req) + response = activities.get_activities(req) activitiy_json_data = response.get_body().decode("utf-8") assert response.status_code == 200 @@ -44,7 +43,7 @@ def test__activity_azure_endpoint__returns_an_activity_with_inactive_status__whe create_temp_activities, ): activities_json, tmp_directory = create_temp_activities - _delete_activity.JSON_PATH = tmp_directory + activities._delete_activity.JSON_PATH = tmp_directory req = func.HttpRequest( method='DELETE', body=None, @@ -52,8 +51,29 @@ def test__activity_azure_endpoint__returns_an_activity_with_inactive_status__whe route_params={"id": activities_json[0]['id']}, ) - response = _delete_activity.delete_activity(req) + response = activities.delete_activity(req) activity_json_data = json.loads(response.get_body().decode("utf-8")) assert response.status_code == 200 assert activity_json_data['status'] == 'inactive' + + +def test__update_activity_azure_endpoint__returns_an_activity__when_found_an_activity_to_update( + create_temp_activities, +): + activities_json, tmp_directory = create_temp_activities + activities._update_activity.JSON_PATH = tmp_directory + activity_data = {"description": Faker().sentence()} + req = func.HttpRequest( + method='PUT', + body=json.dumps(activity_data).encode("utf-8"), + url='/api/activities/', + route_params={"id": activities_json[0]['id']}, + ) + + response = activities.update_activity(req) + activitiy_json_data = response.get_body().decode("utf-8") + new_activity = {**activities_json[0], **activity_data} + + assert response.status_code == 200 + assert activitiy_json_data == json.dumps(new_activity) diff --git a/V2/tests/integration/daos/activities_json_dao_test.py b/V2/tests/integration/daos/activities_json_dao_test.py index 0022900a..00ce99cf 100644 --- a/V2/tests/integration/daos/activities_json_dao_test.py +++ b/V2/tests/integration/daos/activities_json_dao_test.py @@ -6,6 +6,18 @@ import typing +fake_activities = [ + { + 'id': Faker().uuid4(), + 'name': Faker().user_name(), + 'description': Faker().sentence(), + 'deleted': Faker().uuid4(), + 'status': 'active', + 'tenant_id': Faker().uuid4(), + } +] + + @pytest.fixture(name='create_fake_activities') def _create_fake_activities(mocker) -> typing.List[Activity]: def _creator(activities): @@ -20,18 +32,7 @@ def test_get_by_id__returns_an_activity_dto__when_found_one_activity_that_matche create_fake_activities, ): activities_json_dao = ActivitiesJsonDao(Faker().file_path()) - activities = create_fake_activities( - [ - { - "name": "test_name", - "description": "test_description", - "tenant_id": "test_tenant_id", - "id": "test_id", - "deleted": "test_deleted", - "status": "test_status", - } - ] - ) + activities = create_fake_activities(fake_activities) activity_dto = activities.pop() result = activities_json_dao.get_by_id(activity_dto.id) @@ -55,19 +56,7 @@ def test__get_all__returns_a_list_of_activity_dto_objects__when_one_or_more_acti ): activities_json_dao = ActivitiesJsonDao(Faker().file_path()) number_of_activities = 3 - activities = create_fake_activities( - [ - { - "name": "test_name", - "description": "test_description", - "tenant_id": "test_tenant_id", - "id": "test_id", - "deleted": "test_deleted", - "status": "test_status", - } - ] - * number_of_activities - ) + activities = create_fake_activities(fake_activities * number_of_activities) result = activities_json_dao.get_all() @@ -117,3 +106,29 @@ def test_delete__returns_none__when_no_activity_matching_its_id_is_found( result = activities_json_dao.delete(Faker().uuid4()) assert result is None + + +def test_update__returns_an_activity_dto__when_found_one_activity_to_update( + create_fake_activities, +): + activities_json_dao = ActivitiesJsonDao(Faker().file_path()) + activities = create_fake_activities(fake_activities) + activity_dto = activities.pop() + activity_data = {"description": Faker().sentence()} + + result = activities_json_dao.update(activity_dto.id, activity_data) + new_activity = {**activity_dto.__dict__, **activity_data} + + assert result == Activity(**new_activity) + + +def test_update__returns_none__when_doesnt_found_one_activity_to_update( + create_fake_activities, +): + activities_json_dao = ActivitiesJsonDao(Faker().file_path()) + create_fake_activities([]) + activity_data = {"description": Faker().sentence()} + + result = activities_json_dao.update('', activity_data) + + assert result == None diff --git a/V2/tests/unit/services/activity_service_test.py b/V2/tests/unit/services/activity_service_test.py index 5a400b4e..772b3e15 100644 --- a/V2/tests/unit/services/activity_service_test.py +++ b/V2/tests/unit/services/activity_service_test.py @@ -41,3 +41,20 @@ def test__delete_activity__uses_the_activity_dao__to_change_activity_status( assert activity_dao.delete.called assert expected_activity == deleted_activity + + +def test__update_activity__uses_the_activity_dao__to_update_one_activity( + mocker, +): + expected_activity = mocker.Mock() + activity_dao = mocker.Mock( + update=mocker.Mock(return_value=expected_activity) + ) + activity_service = ActivityService(activity_dao) + + updated_activity = activity_service.update( + Faker().uuid4(), Faker().pydict() + ) + + assert activity_dao.update.called + assert expected_activity == updated_activity diff --git a/V2/tests/unit/use_cases/activities_use_case_test.py b/V2/tests/unit/use_cases/activities_use_case_test.py index dfdfcf2b..f3e9a38b 100644 --- a/V2/tests/unit/use_cases/activities_use_case_test.py +++ b/V2/tests/unit/use_cases/activities_use_case_test.py @@ -49,3 +49,20 @@ def test__delete_activity_function__uses_the_activity_service__to_change_activit assert activity_service.delete.called assert expected_activity == deleted_activity + + +def test__update_activity_function__uses_the_activities_service__to_update_an_activity( + mocker: MockFixture, +): + expected_activity = mocker.Mock() + activity_service = mocker.Mock( + update=mocker.Mock(return_value=expected_activity) + ) + + activity_use_case = _use_cases.UpdateActivityUseCase(activity_service) + updated_activity = activity_use_case.update_activity( + fake.uuid4(), fake.pydict() + ) + + assert activity_service.update.called + assert expected_activity == updated_activity diff --git a/V2/time_entries/_application/__init__.py b/V2/time_entries/_application/__init__.py index cb958a05..faa68527 100644 --- a/V2/time_entries/_application/__init__.py +++ b/V2/time_entries/_application/__init__.py @@ -1,2 +1,3 @@ from ._activities import get_activities from ._activities import delete_activity +from ._activities import update_activity diff --git a/V2/time_entries/_application/_activities/__init__.py b/V2/time_entries/_application/_activities/__init__.py index e42c3a12..3482a9c6 100644 --- a/V2/time_entries/_application/_activities/__init__.py +++ b/V2/time_entries/_application/_activities/__init__.py @@ -1,2 +1,3 @@ from ._get_activities import get_activities from ._delete_activity import delete_activity +from ._update_activity import update_activity diff --git a/V2/time_entries/_application/_activities/_update_activity.py b/V2/time_entries/_application/_activities/_update_activity.py new file mode 100644 index 00000000..2aff2881 --- /dev/null +++ b/V2/time_entries/_application/_activities/_update_activity.py @@ -0,0 +1,44 @@ +from time_entries._infrastructure import ActivitiesJsonDao +from time_entries._domain import ActivityService, Activity, _use_cases + +import azure.functions as func +import dataclasses +import json +import logging + +JSON_PATH = ( + 'time_entries/_infrastructure/_data_persistence/activities_data.json' +) + + +def update_activity(req: func.HttpRequest) -> func.HttpResponse: + logging.info( + 'Python HTTP trigger function processed a request to update an activity.' + ) + activity_id = req.route_params.get('id') + activity_data = req.get_json() if req.get_body() else {} + activity_keys = [field.name for field in dataclasses.fields(Activity)] + + if all(key in activity_keys for key in activity_data.keys()): + response = _update(activity_id, activity_data) + status_code = 200 + else: + response = b'Incorrect activity body' + status_code = 400 + + return func.HttpResponse( + body=response, status_code=status_code, mimetype="application/json" + ) + + +def _update(activity_id: str, activity_data: dict) -> str: + activity_use_case = _use_cases.UpdateActivityUseCase( + _create_activity_service(JSON_PATH) + ) + activity = activity_use_case.update_activity(activity_id, activity_data) + return json.dumps(activity.__dict__) if activity else b'Not Found' + + +def _create_activity_service(path: str): + activity_json = ActivitiesJsonDao(path) + return ActivityService(activity_json) diff --git a/V2/time_entries/_domain/__init__.py b/V2/time_entries/_domain/__init__.py index 69cc80f9..f1a97246 100644 --- a/V2/time_entries/_domain/__init__.py +++ b/V2/time_entries/_domain/__init__.py @@ -1,4 +1,8 @@ from ._entities import Activity from ._persistence_contracts import ActivitiesDao from ._services import ActivityService -from ._use_cases import GetActivitiesUseCase, GetActivityUseCase +from ._use_cases import ( + GetActivitiesUseCase, + GetActivityUseCase, + UpdateActivityUseCase, +) diff --git a/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py b/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py index 09de92c0..d2f9e4c7 100644 --- a/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py +++ b/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py @@ -15,3 +15,7 @@ def get_all(self) -> typing.List[Activity]: @abc.abstractmethod def delete(self, id: str) -> Activity: pass + + @abc.abstractmethod + def update(self, id: str, new_activity: dict) -> Activity: + pass diff --git a/V2/time_entries/_domain/_services/_activity.py b/V2/time_entries/_domain/_services/_activity.py index b2294d9a..f4be7836 100644 --- a/V2/time_entries/_domain/_services/_activity.py +++ b/V2/time_entries/_domain/_services/_activity.py @@ -14,3 +14,6 @@ def get_all(self) -> typing.List[Activity]: def delete(self, activity_id: str) -> Activity: return self.activities_dao.delete(activity_id) + + def update(self, activity_id: str, new_activity: dict) -> Activity: + return self.activities_dao.update(activity_id, new_activity) diff --git a/V2/time_entries/_domain/_use_cases/__init__.py b/V2/time_entries/_domain/_use_cases/__init__.py index 476930d0..64c9bb6b 100644 --- a/V2/time_entries/_domain/_use_cases/__init__.py +++ b/V2/time_entries/_domain/_use_cases/__init__.py @@ -1,3 +1,4 @@ from ._get_activities_use_case import GetActivitiesUseCase from ._get_activity_by_id_use_case import GetActivityUseCase from ._delete_activity_use_case import DeleteActivityUseCase +from ._update_activity_use_case import UpdateActivityUseCase diff --git a/V2/time_entries/_domain/_use_cases/_update_activity_use_case.py b/V2/time_entries/_domain/_use_cases/_update_activity_use_case.py new file mode 100644 index 00000000..ea0bc3c5 --- /dev/null +++ b/V2/time_entries/_domain/_use_cases/_update_activity_use_case.py @@ -0,0 +1,11 @@ +from time_entries._domain import ActivityService, Activity + + +class UpdateActivityUseCase: + def __init__(self, activity_service: ActivityService): + self.activity_service = activity_service + + def update_activity( + self, activity_id: str, new_activity: dict + ) -> Activity: + return self.activity_service.update(activity_id, new_activity) diff --git a/V2/time_entries/_infrastructure/_data_persistence/_activities_json_dao.py b/V2/time_entries/_infrastructure/_data_persistence/_activities_json_dao.py index caa89179..dfc41d04 100644 --- a/V2/time_entries/_infrastructure/_data_persistence/_activities_json_dao.py +++ b/V2/time_entries/_infrastructure/_data_persistence/_activities_json_dao.py @@ -51,6 +51,32 @@ def delete(self, activity_id: str) -> Activity: else: return None + def update(self, activity_id: str, new_activity: dict) -> Activity: + activity = self.get_by_id(activity_id) + if not activity: + return None + + new_activity = {**activity.__dict__, **new_activity} + + activities_updated = list( + map( + lambda activity: activity + if activity.get('id') != activity_id + else new_activity, + self.__get_activities_from_file(), + ) + ) + + try: + file = open(self.json_data_file_path, 'w') + json.dump(activities_updated, file) + file.close() + + return self.__create_activity_dto(new_activity) + + except FileNotFoundError: + return None + def __get_activities_from_file(self) -> typing.List[dict]: try: file = open(self.json_data_file_path) diff --git a/V2/time_entries/interface.py b/V2/time_entries/interface.py index f1500529..ffe31e51 100644 --- a/V2/time_entries/interface.py +++ b/V2/time_entries/interface.py @@ -1,2 +1,3 @@ from ._application import get_activities from ._application import delete_activity +from ._application import update_activity From a96b06f3c8dc57c3853144236a226aab6a7ffa0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Soto?= Date: Tue, 26 Oct 2021 18:00:45 -0500 Subject: [PATCH 2/2] refactor: TT-366 Solving SonalCloud code smells --- V2/tests/api/azure/activity_azure_endpoints_test.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/V2/tests/api/azure/activity_azure_endpoints_test.py b/V2/tests/api/azure/activity_azure_endpoints_test.py index 3eab39f1..ef5ba3b0 100644 --- a/V2/tests/api/azure/activity_azure_endpoints_test.py +++ b/V2/tests/api/azure/activity_azure_endpoints_test.py @@ -6,12 +6,15 @@ import typing +ACTIVITY_URL = '/api/activities/' + + def test__activity_azure_endpoint__returns_all_activities( create_temp_activities, ): activities_json, tmp_directory = create_temp_activities activities._get_activities.JSON_PATH = tmp_directory - req = func.HttpRequest(method='GET', body=None, url='/api/activities') + req = func.HttpRequest(method='GET', body=None, url=ACTIVITY_URL) response = activities.get_activities(req) activities_json_data = response.get_body().decode("utf-8") @@ -28,7 +31,7 @@ def test__activity_azure_endpoint__returns_an_activity__when_activity_matches_it req = func.HttpRequest( method='GET', body=None, - url='/api/activities/', + url=ACTIVITY_URL, route_params={"id": activities_json[0]['id']}, ) @@ -47,7 +50,7 @@ def test__activity_azure_endpoint__returns_an_activity_with_inactive_status__whe req = func.HttpRequest( method='DELETE', body=None, - url='/api/activities/', + url=ACTIVITY_URL, route_params={"id": activities_json[0]['id']}, ) @@ -67,7 +70,7 @@ def test__update_activity_azure_endpoint__returns_an_activity__when_found_an_act req = func.HttpRequest( method='PUT', body=json.dumps(activity_data).encode("utf-8"), - url='/api/activities/', + url=ACTIVITY_URL, route_params={"id": activities_json[0]['id']}, )