From 1602427481316a115a470a449d53f88417bfe3c0 Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Tue, 26 Oct 2021 16:36:45 -0500 Subject: [PATCH] feat: TT-365 Method POST activity and create function serverless --- V2/serverless.yml | 16 ++++-- .../azure/activity_azure_endpoints_test.py | 23 ++++++++- .../daos/activities_json_dao_test.py | 16 +++++- .../unit/services/activity_service_test.py | 12 +++++ .../use_cases/activities_use_case_test.py | 15 +++++- V2/time_entries/_application/__init__.py | 3 +- .../_application/_activities/__init__.py | 1 + .../_activities/_create_activity.py | 49 +++++++++++++++++++ .../_persistence_contracts/_activities_dao.py | 6 ++- .../_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 | 13 ++++- .../_data_persistence/activities_data.json | 3 +- V2/time_entries/interface.py | 3 +- requirements/time_tracker_api/dev.txt | 1 + requirements/time_tracker_api/prod.txt | 3 +- 17 files changed, 164 insertions(+), 15 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 5c08f749..47db2eab 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -49,10 +49,20 @@ functions: delete_activity: handler: time_entries/interface.delete_activity + events: + - http: true + x-azure-settings: + methods: + - DELETE + route: activities/{id} + authLevel: anonymous + + create_activity: + handler: time_entries/interface.create_activity events: - http: true x-azure-settings: methods: - - DELETE - route: activities/{id} - authLevel: anonymous \ No newline at end of file + - 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 824a52d4..fc1346f6 100644 --- a/V2/tests/api/azure/activity_azure_endpoints_test.py +++ b/V2/tests/api/azure/activity_azure_endpoints_test.py @@ -1,11 +1,12 @@ from time_entries._application._activities import ( _get_activities, _delete_activity, + _create_activity ) import azure.functions as func import json import typing - +from faker import Faker def test__activity_azure_endpoint__returns_all_activities( create_temp_activities, @@ -57,3 +58,23 @@ def test__activity_azure_endpoint__returns_an_activity_with_inactive_status__whe assert response.status_code == 200 assert activity_json_data['status'] == 'inactive' +def test__activity_azure_endpoint__creates_an_activity__when_activity_has_all_attributes( + create_temp_activities, +): + activities_json, tmp_directory = create_temp_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 = _create_activity.create_activity(req) + activitiy_json_data = response.get_body() + + assert response.status_code == 200 + assert activitiy_json_data == body + diff --git a/V2/tests/integration/daos/activities_json_dao_test.py b/V2/tests/integration/daos/activities_json_dao_test.py index 0022900a..6bc069d5 100644 --- a/V2/tests/integration/daos/activities_json_dao_test.py +++ b/V2/tests/integration/daos/activities_json_dao_test.py @@ -84,6 +84,20 @@ def test_get_all__returns_an_empty_list__when_doesnt_found_any_activities( assert result == activities +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) def test_delete__returns_an_activity_with_inactive_status__when_an_activity_matching_its_id_is_found( create_fake_activities, @@ -116,4 +130,4 @@ def test_delete__returns_none__when_no_activity_matching_its_id_is_found( result = activities_json_dao.delete(Faker().uuid4()) - assert result is None + assert result is None \ 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 5a400b4e..ffcca7f0 100644 --- a/V2/tests/unit/services/activity_service_test.py +++ b/V2/tests/unit/services/activity_service_test.py @@ -41,3 +41,15 @@ def test__delete_activity__uses_the_activity_dao__to_change_activity_status( assert activity_dao.delete.called assert expected_activity == deleted_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 dfdfcf2b..5c423ee7 100644 --- a/V2/tests/unit/use_cases/activities_use_case_test.py +++ b/V2/tests/unit/use_cases/activities_use_case_test.py @@ -35,6 +35,19 @@ def test__get_activity_by_id_function__uses_the_activity_service__to_retrieve_ac assert activity_service.get_by_id.called 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, @@ -48,4 +61,4 @@ def test__delete_activity_function__uses_the_activity_service__to_change_activit deleted_activity = activity_use_case.delete_activity(fake.uuid4()) assert activity_service.delete.called - assert expected_activity == deleted_activity + assert expected_activity == deleted_activity \ No newline at end of file diff --git a/V2/time_entries/_application/__init__.py b/V2/time_entries/_application/__init__.py index cb958a05..30beae97 100644 --- a/V2/time_entries/_application/__init__.py +++ b/V2/time_entries/_application/__init__.py @@ -1,2 +1 @@ -from ._activities import get_activities -from ._activities import delete_activity +from ._activities import get_activities, create_activity, delete_activity diff --git a/V2/time_entries/_application/_activities/__init__.py b/V2/time_entries/_application/_activities/__init__.py index e42c3a12..62697ad0 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 ._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..bcf71007 --- /dev/null +++ b/V2/time_entries/_application/_activities/_create_activity.py @@ -0,0 +1,49 @@ +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 = ( + + '::WqV2/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 09de92c0..cd0e959e 100644 --- a/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py +++ b/V2/time_entries/_domain/_persistence_contracts/_activities_dao.py @@ -12,6 +12,10 @@ def get_by_id(self, id: str) -> Activity: def get_all(self) -> typing.List[Activity]: pass + @abc.abstractmethod + def create_activity(self, activity_data: dict) -> Activity: + pass + @abc.abstractmethod def delete(self, id: str) -> Activity: - pass + pass \ No newline at end of file diff --git a/V2/time_entries/_domain/_services/_activity.py b/V2/time_entries/_domain/_services/_activity.py index b2294d9a..094a5311 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 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 476930d0..d98a44bb 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 ._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 caa89179..8c94b289 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 @@ -50,6 +49,18 @@ def delete(self, activity_id: str) -> Activity: else: 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: diff --git a/V2/time_entries/_infrastructure/_data_persistence/activities_data.json b/V2/time_entries/_infrastructure/_data_persistence/activities_data.json index 0d949902..16314929 100644 --- a/V2/time_entries/_infrastructure/_data_persistence/activities_data.json +++ b/V2/time_entries/_infrastructure/_data_persistence/activities_data.json @@ -62,5 +62,4 @@ "status": "active", "_ts": 1632331515 } -] - +] \ No newline at end of file diff --git a/V2/time_entries/interface.py b/V2/time_entries/interface.py index f1500529..3c889760 100644 --- a/V2/time_entries/interface.py +++ b/V2/time_entries/interface.py @@ -1,2 +1 @@ -from ._application import get_activities -from ._application import delete_activity +from ._application import get_activities, create_activity, delete_activity diff --git a/requirements/time_tracker_api/dev.txt b/requirements/time_tracker_api/dev.txt index 302acb78..dd91ba3c 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 # Mocking 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