From 003376dcf979ebe02f3de329a60070e22323098d Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Thu, 28 Oct 2021 15:58:32 -0500 Subject: [PATCH 1/4] feat: TT-365 Method POST activity and create function serverless --- V2/serverless.yml | 12 ++++- .../azure/activity_azure_endpoints_test.py | 20 ++++++++ .../daos/activities_json_dao_test.py | 15 ++++++ .../unit/services/activity_service_test.py | 12 +++++ .../use_cases/activities_use_case_test.py | 15 +++++- V2/time_entries/_application/__init__.py | 1 + .../_application/_activities/__init__.py | 1 + .../_activities/_create_activity.py | 48 +++++++++++++++++++ .../_persistence_contracts/_activities_dao.py | 8 ++++ .../_domain/_services/_activity.py | 3 ++ .../_domain/_use_cases/__init__.py | 1 + .../_use_cases/_create_activity_use_case.py | 11 +++++ .../_data_persistence/_activities_json_dao.py | 14 +++++- .../_data_persistence/activities_data.json | 1 - V2/time_entries/interface.py | 1 + requirements/time_tracker_api/dev.txt | 1 + requirements/time_tracker_api/prod.txt | 3 +- 17 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 V2/time_entries/_application/_activities/_create_activity.py create mode 100644 V2/time_entries/_domain/_use_cases/_create_activity_use_case.py diff --git a/V2/serverless.yml b/V2/serverless.yml index 223c8a33..0d085c36 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -63,6 +63,16 @@ functions: - http: true x-azure-settings: methods: - - PUT + - PUT route: activities/{id} + authLevel: anonymous + + create_activity: + handler: time_entries/interface.create_activity + events: + - http: true + x-azure-settings: + methods: + - POST + route: activities/ 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 ef5ba3b0..98d853f1 100644 --- a/V2/tests/api/azure/activity_azure_endpoints_test.py +++ b/V2/tests/api/azure/activity_azure_endpoints_test.py @@ -80,3 +80,23 @@ def test__update_activity_azure_endpoint__returns_an_activity__when_found_an_act assert response.status_code == 200 assert activitiy_json_data == json.dumps(new_activity) + +def test__activity_azure_endpoint__creates_an_activity__when_activity_has_all_attributes( + create_temp_activities, + ): + activities_json, tmp_directory = create_temp_activities + activities._create_activity.JSON_PATH = tmp_directory + + activity_body = {'id': Faker().uuid4(), 'name': Faker().user_name(), 'description': Faker().sentence(),'deleted': Faker().uuid4() ,'status': 'active', 'tenant_id': Faker().uuid4()} + body = json.dumps(activity_body).encode("utf-8") + req = func.HttpRequest( + method='POST', + body= body, + url='/api/activities/', + ) + + response = activities._create_activity.create_activity(req) + activitiy_json_data = response.get_body() + + assert response.status_code == 200 + assert activitiy_json_data == body \ No newline at end of file diff --git a/V2/tests/integration/daos/activities_json_dao_test.py b/V2/tests/integration/daos/activities_json_dao_test.py index 00ce99cf..f2b0dacd 100644 --- a/V2/tests/integration/daos/activities_json_dao_test.py +++ b/V2/tests/integration/daos/activities_json_dao_test.py @@ -132,3 +132,18 @@ def test_update__returns_none__when_doesnt_found_one_activity_to_update( result = activities_json_dao.update('', activity_data) assert result == None + +def test_create_activity__returns_an_activity_dto__when_create_an_activity_that_matches_attributes(create_fake_activities): + create_fake_activities([]) + + activities_json_dao = ActivitiesJsonDao(Faker().file_path()) + activity_data = { + "name": "test_name", + "description": "test_description", + "tenant_id": "test_tenant_id", + "id": "test_id", + "deleted": "test_deleted", + "status": "test_status", + } + result = activities_json_dao.create_activity(activity_data) + assert result == Activity(**activity_data) \ No newline at end of file diff --git a/V2/tests/unit/services/activity_service_test.py b/V2/tests/unit/services/activity_service_test.py index 772b3e15..9315d24f 100644 --- a/V2/tests/unit/services/activity_service_test.py +++ b/V2/tests/unit/services/activity_service_test.py @@ -58,3 +58,15 @@ def test__update_activity__uses_the_activity_dao__to_update_one_activity( assert activity_dao.update.called assert expected_activity == updated_activity + +def test__create_activity__uses_the_activity_dao__to_create_an_activity(mocker): + expected_activity = mocker.Mock() + activity_dao = mocker.Mock( + create_activity=mocker.Mock(return_value=expected_activity) + ) + activity_service = ActivityService(activity_dao) + + actual_activity = activity_service.create_activity(Faker().pydict()) + + assert activity_dao.create_activity.called + assert expected_activity == actual_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 f3e9a38b..793c32d6 100644 --- a/V2/tests/unit/use_cases/activities_use_case_test.py +++ b/V2/tests/unit/use_cases/activities_use_case_test.py @@ -36,6 +36,20 @@ def test__get_activity_by_id_function__uses_the_activity_service__to_retrieve_ac assert expected_activity == actual_activity +def test__create_activity_function__uses_the_activities_service__to_create_activity( + mocker: MockFixture, + ): + expected_activity = mocker.Mock() + activity_service = mocker.Mock( + create_activity=mocker.Mock(return_value=expected_activity) + ) + + activity_use_case = _use_cases.CreateActivityUseCase(activity_service) + actual_activity = activity_use_case.create_activity(fake.pydict()) + + assert activity_service.create_activity.called + assert expected_activity == actual_activity + def test__delete_activity_function__uses_the_activity_service__to_change_activity_status( mocker: MockFixture, ): @@ -50,7 +64,6 @@ 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, ): diff --git a/V2/time_entries/_application/__init__.py b/V2/time_entries/_application/__init__.py index faa68527..c8f26492 100644 --- a/V2/time_entries/_application/__init__.py +++ b/V2/time_entries/_application/__init__.py @@ -1,3 +1,4 @@ from ._activities import get_activities from ._activities import delete_activity from ._activities import update_activity +from ._activities import create_activity diff --git a/V2/time_entries/_application/_activities/__init__.py b/V2/time_entries/_application/_activities/__init__.py index 3482a9c6..ab7d3844 100644 --- a/V2/time_entries/_application/_activities/__init__.py +++ b/V2/time_entries/_application/_activities/__init__.py @@ -1,3 +1,4 @@ from ._get_activities import get_activities from ._delete_activity import delete_activity from ._update_activity import update_activity +from ._create_activity import create_activity diff --git a/V2/time_entries/_application/_activities/_create_activity.py b/V2/time_entries/_application/_activities/_create_activity.py new file mode 100644 index 00000000..88bb8181 --- /dev/null +++ b/V2/time_entries/_application/_activities/_create_activity.py @@ -0,0 +1,48 @@ +from time_entries._infrastructure import ActivitiesJsonDao +from time_entries._domain import ActivityService, _use_cases, Activity + +import azure.functions as func +import json +import logging +import dataclasses + + +JSON_PATH = ( + 'time_entries/_infrastructure/_data_persistence/activities_data.json' +) + + + +def create_activity(req: func.HttpRequest) -> func.HttpResponse: + logging.info( + 'Python HTTP trigger function processed a request to create an activity.' + ) + activity_data = req.get_json() + status_code = 200 + if _validate_activity(activity_data): + response = _create_activity(activity_data) + else: + status_code = 404 + response = b'Not possible to create activity, attributes are not correct ' + + return func.HttpResponse( + body=response, status_code=status_code, mimetype="application/json" + ) + +def _create_activity(activity_data: dict) -> str: + activity_use_case = _use_cases.CreateActivityUseCase( + _create_activity_service(JSON_PATH) + ) + activity = activity_use_case.create_activity(activity_data) + return json.dumps(activity.__dict__) if activity else b'Not Found' + +def _validate_activity(activity_data: dict) -> bool: + activity_keys = [field.name for field in dataclasses.fields(Activity)] + new_activity_keys = list(activity_data.keys()) + return all(map(lambda key: key in activity_keys, new_activity_keys)) and len(activity_keys) == len(new_activity_keys) + +def _create_activity_service(path: str): + activity_json = ActivitiesJsonDao(path) + return ActivityService(activity_json) + + diff --git a/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py b/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py index d2f9e4c7..2037841d 100644 --- a/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py +++ b/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py @@ -19,3 +19,11 @@ def delete(self, id: str) -> Activity: @abc.abstractmethod def update(self, id: str, new_activity: dict) -> Activity: pass + + @abc.abstractmethod + def create_activity(self, activity_data: dict) -> Activity: + pass + + @abc.abstractmethod + def delete(self, id: str) -> Activity: + pass diff --git a/V2/time_entries/_domain/_services/_activity.py b/V2/time_entries/_domain/_services/_activity.py index f4be7836..8d29a7ab 100644 --- a/V2/time_entries/_domain/_services/_activity.py +++ b/V2/time_entries/_domain/_services/_activity.py @@ -17,3 +17,6 @@ def delete(self, activity_id: str) -> Activity: def update(self, activity_id: str, new_activity: dict) -> Activity: return self.activities_dao.update(activity_id, new_activity) + + def create_activity(self, activity_data: dict) -> Activity: + return self.activities_dao.create_activity(activity_data) diff --git a/V2/time_entries/_domain/_use_cases/__init__.py b/V2/time_entries/_domain/_use_cases/__init__.py index 64c9bb6b..642d2425 100644 --- a/V2/time_entries/_domain/_use_cases/__init__.py +++ b/V2/time_entries/_domain/_use_cases/__init__.py @@ -2,3 +2,4 @@ from ._get_activity_by_id_use_case import GetActivityUseCase from ._delete_activity_use_case import DeleteActivityUseCase from ._update_activity_use_case import UpdateActivityUseCase +from ._create_activity_use_case import CreateActivityUseCase diff --git a/V2/time_entries/_domain/_use_cases/_create_activity_use_case.py b/V2/time_entries/_domain/_use_cases/_create_activity_use_case.py new file mode 100644 index 00000000..a7f7a66e --- /dev/null +++ b/V2/time_entries/_domain/_use_cases/_create_activity_use_case.py @@ -0,0 +1,11 @@ +from time_entries._domain import ActivityService, Activity +import typing + + +class CreateActivityUseCase: + def __init__(self, activity_service: ActivityService): + self.activity_service = activity_service + + def create_activity(self, activity_data: dict ) -> Activity: + return self.activity_service.create_activity(activity_data) + 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 dfc41d04..ab8f5765 100644 --- a/V2/time_entries/_infrastructure/_data_persistence/_activities_json_dao.py +++ b/V2/time_entries/_infrastructure/_data_persistence/_activities_json_dao.py @@ -3,7 +3,6 @@ import json import typing - class ActivitiesJsonDao(ActivitiesDao): def __init__(self, json_data_file_path: str): self.json_data_file_path = json_data_file_path @@ -77,6 +76,19 @@ def update(self, activity_id: str, new_activity: dict) -> Activity: except FileNotFoundError: return None + def create_activity(self, activity_data: dict) -> Activity: + activities = self.__get_activities_from_file() + activities.append(activity_data) + + try: + with open(self.json_data_file_path, 'w') as outfile: + json.dump(activities, outfile) + + return self.__create_activity_dto(activity_data) + except FileNotFoundError: + print("Can not create activity") + + def __get_activities_from_file(self) -> typing.List[dict]: try: file = open(self.json_data_file_path) diff --git a/V2/time_entries/_infrastructure/_data_persistence/activities_data.json b/V2/time_entries/_infrastructure/_data_persistence/activities_data.json index 0d949902..961251db 100644 --- a/V2/time_entries/_infrastructure/_data_persistence/activities_data.json +++ b/V2/time_entries/_infrastructure/_data_persistence/activities_data.json @@ -63,4 +63,3 @@ "_ts": 1632331515 } ] - diff --git a/V2/time_entries/interface.py b/V2/time_entries/interface.py index ffe31e51..1f1fc805 100644 --- a/V2/time_entries/interface.py +++ b/V2/time_entries/interface.py @@ -1,3 +1,4 @@ from ._application import get_activities from ._application import delete_activity from ._application import update_activity +from ._application import create_activity \ No newline at end of file diff --git a/requirements/time_tracker_api/dev.txt b/requirements/time_tracker_api/dev.txt index 2e5aee81..9657c071 100644 --- a/requirements/time_tracker_api/dev.txt +++ b/requirements/time_tracker_api/dev.txt @@ -6,6 +6,7 @@ # For development # Tests +Faker==4.0.2 pytest==5.2.0 Flask_sqlalchemy diff --git a/requirements/time_tracker_api/prod.txt b/requirements/time_tracker_api/prod.txt index 6fd17f94..77ed3a0a 100644 --- a/requirements/time_tracker_api/prod.txt +++ b/requirements/time_tracker_api/prod.txt @@ -3,11 +3,12 @@ # Dependencies -r ../commons.txt -r ../azure_cosmos.txt --r ../sql_db.txt +# -r ../sql_db.txt # For production releases #Required by Flask +Faker==4.0.2 Flask==1.1.1 Flask-WTF==0.15.1 flake8==3.7.9 From ee50bfd0f0ac6f4990d5a5c549b4c9ef9233cbc2 Mon Sep 17 00:00:00 2001 From: Daniela Garcia Date: Mon, 8 Nov 2021 17:53:30 -0500 Subject: [PATCH 2/4] fix: TT-393 change user id variable to list --- utils/azure_users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/azure_users.py b/utils/azure_users.py index ba271a4d..9b26bd26 100644 --- a/utils/azure_users.py +++ b/utils/azure_users.py @@ -23,7 +23,7 @@ class MSConfig: SECRET = os.environ.get('MS_SECRET') SCOPE = os.environ.get('MS_SCOPE') ENDPOINT = os.environ.get('MS_ENDPOINT') - USERID = os.environ.get('USERID') + USERID = json.loads(os.environ.get('USERID')) class BearerAuth(requests.auth.AuthBase): @@ -263,7 +263,7 @@ def get_groups_and_users(self): [member['objectId'] for member in item['members']], ) result = list(map(parse_item, response.json()['value'])) - result[0][1].append(self.config.USERID) + result[0][1].extend(self.config.USERID) return result From 4b071a1ddbc5e1271e10399c9a799787dd128e69 Mon Sep 17 00:00:00 2001 From: mandres2015 Date: Tue, 9 Nov 2021 17:33:45 -0500 Subject: [PATCH 3/4] TT-393 added list to userid --- utils/azure_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/azure_users.py b/utils/azure_users.py index 9b26bd26..fc8c2c7f 100644 --- a/utils/azure_users.py +++ b/utils/azure_users.py @@ -23,7 +23,7 @@ class MSConfig: SECRET = os.environ.get('MS_SECRET') SCOPE = os.environ.get('MS_SCOPE') ENDPOINT = os.environ.get('MS_ENDPOINT') - USERID = json.loads(os.environ.get('USERID')) + USERID = os.environ.get('USERID').split(" ") class BearerAuth(requests.auth.AuthBase): From 6457154ae435e086a45961bf3d10b56919f80bbb Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Tue, 23 Nov 2021 15:49:38 -0500 Subject: [PATCH 4/4] docs: TT-393 add new title --- utils/azure_users.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/utils/azure_users.py b/utils/azure_users.py index fc8c2c7f..2ab70bd3 100644 --- a/utils/azure_users.py +++ b/utils/azure_users.py @@ -23,7 +23,7 @@ class MSConfig: SECRET = os.environ.get('MS_SECRET') SCOPE = os.environ.get('MS_SCOPE') ENDPOINT = os.environ.get('MS_ENDPOINT') - USERID = os.environ.get('USERID').split(" ") + USERID = os.environ.get('USERID') class BearerAuth(requests.auth.AuthBase): @@ -263,7 +263,8 @@ def get_groups_and_users(self): [member['objectId'] for member in item['members']], ) result = list(map(parse_item, response.json()['value'])) - result[0][1].extend(self.config.USERID) + list_of_userid = self.config.USERID.split(" ") + result[0][1].extend(list_of_userid) return result