From 5e31fe13c19100593a30c0e23bbbb0c111a72cd8 Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Thu, 25 Nov 2021 19:17:53 -0500 Subject: [PATCH] eat: TT-404 GET Time Entries --- V2/serverless.yml | 14 +++- .../azure/time_entry_azure_endpoints_test.py | 65 ++++++++++++++++++- .../integration/daos/time_entries_dao_test.py | 55 ++++++++++++++++ .../unit/services/time_entry_service_test.py | 26 ++++++++ .../use_cases/time_entries_use_case_test.py | 32 +++++++++ .../time_entries/_application/__init__.py | 1 + .../_application/_time_entries/__init__.py | 1 + .../_time_entries/_get_time_entries.py | 61 +++++++++++++++++ .../time_entries/_domain/__init__.py | 4 +- .../_time_entries_dao.py | 8 +++ .../_domain/_services/_time_entry.py | 9 ++- .../_domain/_use_cases/__init__.py | 2 + .../_get_time_entry_by_id_use_case.py | 9 +++ .../_use_cases/_get_time_entry_use_case.py | 11 ++++ .../_data_persistence/__init__.py | 2 +- ...ntries_dao.py => _time_entries_sql_dao.py} | 15 +++++ V2/time_tracker/time_entries/interface.py | 1 + 17 files changed, 308 insertions(+), 8 deletions(-) create mode 100644 V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py create mode 100644 V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py create mode 100644 V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py rename V2/time_tracker/time_entries/_infrastructure/_data_persistence/{_time_entries_dao.py => _time_entries_sql_dao.py} (85%) diff --git a/V2/serverless.yml b/V2/serverless.yml index e02e8fb6..ba8edb52 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -63,9 +63,9 @@ functions: - http: true x-azure-settings: methods: - - PUT + - PUT route: activities/{id} - authLevel: anonymous + authLevel: anonymous create_activity: handler: time_tracker/activities/interface.create_activity @@ -87,6 +87,16 @@ functions: route: time-entries/ authLevel: anonymous + get_time_entries: + handler: time_tracker/time_entries/interface.get_time_entries + events: + - http: true + x-azure-settings: + methods: + - GET + route: time-entries/{id:?} + authLevel: anonymous + delete_time_entry: handler: time_tracker/time_entries/interface.delete_time_entry events: diff --git a/V2/tests/api/azure/time_entry_azure_endpoints_test.py b/V2/tests/api/azure/time_entry_azure_endpoints_test.py index f57db585..fcc8dea0 100644 --- a/V2/tests/api/azure/time_entry_azure_endpoints_test.py +++ b/V2/tests/api/azure/time_entry_azure_endpoints_test.py @@ -1,6 +1,7 @@ import pytest import json from faker import Faker +from http import HTTPStatus import azure.functions as func @@ -39,7 +40,7 @@ def test__time_entry_azure_endpoint__creates_an_time_entry__when_time_entry_has_ time_entry_json_data = json.loads(response.get_body()) time_entry_body['id'] = time_entry_json_data['id'] - assert response.status_code == 201 + assert response.status_code == HTTPStatus.CREATED assert time_entry_json_data == time_entry_body @@ -60,7 +61,7 @@ def test__delete_time_entries_azure_endpoint__returns_an_time_entry_with_true_de response = azure_time_entries._delete_time_entry.delete_time_entry(req) time_entry_json_data = json.loads(response.get_body().decode("utf-8")) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK assert time_entry_json_data['deleted'] is True @@ -75,7 +76,65 @@ def test__delete_time_entries_azure_endpoint__returns_a_status_code_400__when_ti response = azure_time_entries._delete_time_entry.delete_time_entry(req) - assert response.status_code == 400 + assert response.status_code == HTTPStatus.BAD_REQUEST + assert response.get_body() == b'Invalid Format ID' + + +def test__time_entry_azure_endpoint__returns_all_time_entries( + test_db, time_entry_factory, insert_time_entry, activity_factory, insert_activity +): + inserted_activity = insert_activity(activity_factory(), test_db) + time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id) + inserted_time_entries = insert_time_entry(time_entries_to_insert, test_db).__dict__ + + req = func.HttpRequest(method="GET", body=None, url=TIME_ENTRY_URL) + + response = azure_time_entries.get_time_entries(req) + time_entries_json_data = response.get_body().decode("utf-8") + time_entry_list = json.loads(time_entries_json_data) + + assert response.status_code == HTTPStatus.OK + assert time_entry_list.pop() == inserted_time_entries + + +def test__time_entry_azure_endpoint__returns_an_time_entry__when_time_entry_matches_its_id( + test_db, time_entry_factory, insert_time_entry, activity_factory, insert_activity +): + inserted_activity = insert_activity(activity_factory(), test_db) + time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id) + inserted_time_entries = insert_time_entry(time_entries_to_insert, test_db).__dict__ + + req = func.HttpRequest( + method="GET", + body=None, + url=TIME_ENTRY_URL, + route_params={"id": inserted_time_entries["id"]}, + ) + + response = azure_time_entries.get_time_entries(req) + time_entry_json_data = response.get_body().decode("utf-8") + + assert response.status_code == HTTPStatus.OK + assert time_entry_json_data == json.dumps(inserted_time_entries) + + +def test__get_time_entries_azure_endpoint__returns_a_status_code_400__when_time_entry_recive_invalid_id( + test_db, time_entry_factory, insert_time_entry, activity_factory, insert_activity +): + inserted_activity = insert_activity(activity_factory(), test_db) + time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id) + insert_time_entry(time_entries_to_insert, test_db).__dict__ + + req = func.HttpRequest( + method="GET", + body=None, + url=TIME_ENTRY_URL, + route_params={"id": "invalid id"}, + ) + + response = azure_time_entries.get_time_entries(req) + + assert response.status_code == HTTPStatus.BAD_REQUEST assert response.get_body() == b'Invalid Format ID' diff --git a/V2/tests/integration/daos/time_entries_dao_test.py b/V2/tests/integration/daos/time_entries_dao_test.py index fbe5a7ed..e78af556 100644 --- a/V2/tests/integration/daos/time_entries_dao_test.py +++ b/V2/tests/integration/daos/time_entries_dao_test.py @@ -1,4 +1,6 @@ import pytest +import typing + from faker import Faker import time_tracker.time_entries._domain as domain @@ -98,3 +100,56 @@ def test_update__returns_none__when_doesnt_found_one_time_entry_to_update( time_entry = dao.update(0, inserted_time_entries) assert time_entry is None + + +def test__get_all__returns_a_list_of_time_entries_dto_objects__when_one_or_more_time_entries_are_found_in_sql_database( + test_db, create_fake_dao, time_entry_factory, insert_activity, activity_factory +): + + dao = create_fake_dao(test_db) + inserted_activity = insert_activity(activity_factory(), dao.db) + time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id) + inserted_time_entries = [dao.create(time_entries_to_insert)] + + time_entry = dao.get_all() + + assert isinstance(time_entry, typing.List) + assert time_entry == inserted_time_entries + + +def test__get_all__returns_an_empty_list__when_doesnt_found_any_time_entries_in_sql_database( + test_db, create_fake_dao, insert_activity, activity_factory +): + dao = create_fake_dao(test_db) + insert_activity(activity_factory(), dao.db) + + time_entry = dao.get_all() + assert time_entry == [] + + +def test__get_by_id__returns_a_time_entry_dto__when_found_one_time_entry_that_match_id_with_sql_database( + test_db, create_fake_dao, time_entry_factory, insert_activity, activity_factory +): + dao = create_fake_dao(test_db) + inserted_activity = insert_activity(activity_factory(), dao.db) + time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id) + inserted_time_entries = dao.create(time_entries_to_insert) + + time_entry = dao.get_by_id(time_entries_to_insert.id) + + assert isinstance(time_entry, domain.TimeEntry) + assert time_entry.id == inserted_time_entries.id + assert time_entry == inserted_time_entries + + +def test__get_by_id__returns_none__when_no_time_entry_matches_by_id( + test_db, create_fake_dao, time_entry_factory, insert_activity, activity_factory +): + dao = create_fake_dao(test_db) + inserted_activity = insert_activity(activity_factory(), dao.db) + time_entries_to_insert = time_entry_factory(activity_id=inserted_activity.id) + dao.create(time_entries_to_insert) + + time_entry = dao.get_by_id(Faker().pyint()) + + assert time_entry is None diff --git a/V2/tests/unit/services/time_entry_service_test.py b/V2/tests/unit/services/time_entry_service_test.py index 0952f8a9..1992324f 100644 --- a/V2/tests/unit/services/time_entry_service_test.py +++ b/V2/tests/unit/services/time_entry_service_test.py @@ -46,3 +46,29 @@ def test__update_time_entry__uses_the_time_entry_dao__to_update_one_time_entry( assert time_entry_dao.update.called assert expected_time_entry == updated_time_entry + + +def test__get_all__uses_the_time_entry_dao__to_retrieve_time_entries(mocker): + expected_time_entries = mocker.Mock() + time_entry_dao = mocker.Mock( + get_all=mocker.Mock(return_value=expected_time_entries) + ) + time_activity_service = TimeEntryService(time_entry_dao) + + actual_activities = time_activity_service.get_all() + + assert time_entry_dao.get_all.called + assert expected_time_entries == actual_activities + + +def test__get_by_id__uses_the_time_entry_dao__to_retrieve_one_time_entry(mocker): + expected_time_entry = mocker.Mock() + time_entry_dao = mocker.Mock( + get_by_id=mocker.Mock(return_value=expected_time_entry) + ) + time_entry_service = TimeEntryService(time_entry_dao) + + actual_time_entry = time_entry_service.get_by_id(Faker().uuid4()) + + assert time_entry_dao.get_by_id.called + assert expected_time_entry == actual_time_entry diff --git a/V2/tests/unit/use_cases/time_entries_use_case_test.py b/V2/tests/unit/use_cases/time_entries_use_case_test.py index 1a679f37..05937789 100644 --- a/V2/tests/unit/use_cases/time_entries_use_case_test.py +++ b/V2/tests/unit/use_cases/time_entries_use_case_test.py @@ -3,6 +3,8 @@ from time_tracker.time_entries._domain import _use_cases +fake = Faker() + def test__create_time_entry_function__uses_the_time_entries_service__to_create_time_entry( mocker: MockFixture, time_entry_factory @@ -43,3 +45,33 @@ def test__update_time_entries_function__uses_the_time_entry_service__to_update_a assert time_entry_service.update.called assert expected_time_entry == updated_time_entry + + +def test__get_all_time_entries_function__using_the_use_case_get_time_entries__to_get_all_time_entries( + mocker: MockFixture, +): + expected_time_entries = mocker.Mock() + time_entry_service = mocker.Mock( + get_all=mocker.Mock(return_value=expected_time_entries) + ) + + time_entries_use_case = _use_cases.GetTimeEntriesUseCase(time_entry_service) + actual_time_entries = time_entries_use_case.get_time_entries() + + assert time_entry_service.get_all.called + assert expected_time_entries == actual_time_entries + + +def test__get_time_entry_by_id_function__uses_the_time_entry_service__to_retrieve_time_entry( + mocker: MockFixture, +): + expected_time_entries = mocker.Mock() + time_entry_service = mocker.Mock( + get_by_id=mocker.Mock(return_value=expected_time_entries) + ) + + time_entry_use_case = _use_cases.GetTimeEntryUseCase(time_entry_service) + actual_time_entry = time_entry_use_case.get_time_entry_by_id(fake.uuid4()) + + assert time_entry_service.get_by_id.called + assert expected_time_entries == actual_time_entry diff --git a/V2/time_tracker/time_entries/_application/__init__.py b/V2/time_tracker/time_entries/_application/__init__.py index 0ca4e272..eb817c22 100644 --- a/V2/time_tracker/time_entries/_application/__init__.py +++ b/V2/time_tracker/time_entries/_application/__init__.py @@ -2,3 +2,4 @@ from ._time_entries import create_time_entry from ._time_entries import delete_time_entry from ._time_entries import update_time_entry +from ._time_entries import get_time_entries diff --git a/V2/time_tracker/time_entries/_application/_time_entries/__init__.py b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py index 0f6cf2db..382fbbe4 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/__init__.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py @@ -2,3 +2,4 @@ from ._create_time_entry import create_time_entry from ._delete_time_entry import delete_time_entry from ._update_time_entry import update_time_entry +from ._get_time_entries import get_time_entries diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py new file mode 100644 index 00000000..37574d32 --- /dev/null +++ b/V2/time_tracker/time_entries/_application/_time_entries/_get_time_entries.py @@ -0,0 +1,61 @@ +import json +from http import HTTPStatus + +import azure.functions as func + +from time_tracker.time_entries._infrastructure import TimeEntriesSQLDao +from time_tracker.time_entries._domain import TimeEntryService, _use_cases +from time_tracker._infrastructure import DB + + +NOT_FOUND = b'Not Found' +INVALID_FORMAT_ID = b'Invalid Format ID' + + +def get_time_entries(req: func.HttpRequest) -> func.HttpResponse: + + time_entry_id = req.route_params.get('id') + status_code = HTTPStatus.OK + + if time_entry_id: + try: + response = _get_by_id(int(time_entry_id)) + if response == NOT_FOUND: + status_code = HTTPStatus.NOT_FOUND + except ValueError: + response = INVALID_FORMAT_ID + status_code = HTTPStatus.BAD_REQUEST + else: + response = _get_all() + + return func.HttpResponse( + body=response, status_code=status_code, mimetype="application/json" + ) + + +def _get_by_id(id: int) -> str: + database = DB() + time_entry_use_case = _use_cases.GetTimeEntryUseCase( + _create_time_entry_service(database) + ) + time_entry = time_entry_use_case.get_time_entry_by_id(id) + + return json.dumps(time_entry.__dict__) if time_entry else NOT_FOUND + + +def _get_all() -> str: + database = DB() + time_entries_use_case = _use_cases.GetTimeEntriesUseCase( + _create_time_entry_service(database) + ) + return json.dumps( + [ + time_entry.__dict__ + for time_entry in time_entries_use_case.get_time_entries() + ] + ) + + +def _create_time_entry_service(db: DB): + time_entry_sql = TimeEntriesSQLDao(db) + return TimeEntryService(time_entry_sql) diff --git a/V2/time_tracker/time_entries/_domain/__init__.py b/V2/time_tracker/time_entries/_domain/__init__.py index de58675c..2034f8d3 100644 --- a/V2/time_tracker/time_entries/_domain/__init__.py +++ b/V2/time_tracker/time_entries/_domain/__init__.py @@ -6,4 +6,6 @@ CreateTimeEntryUseCase, DeleteTimeEntryUseCase, UpdateTimeEntryUseCase, -) \ No newline at end of file + GetTimeEntriesUseCase, + GetTimeEntryUseCase +) diff --git a/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py index 8c1dc9d9..ca4ceb98 100644 --- a/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py +++ b/V2/time_tracker/time_entries/_domain/_persistence_contracts/_time_entries_dao.py @@ -1,4 +1,5 @@ import abc +import typing from time_tracker.time_entries._domain import TimeEntry @@ -15,3 +16,10 @@ def delete(self, id: int) -> TimeEntry: @abc.abstractmethod def update(self, id: int, new_time_entry: dict) -> TimeEntry: pass + + def get_by_id(self, id: int) -> TimeEntry: + pass + + @abc.abstractmethod + def get_all(self) -> typing.List[TimeEntry]: + pass diff --git a/V2/time_tracker/time_entries/_domain/_services/_time_entry.py b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py index 5c32c1e3..5b3f4115 100644 --- a/V2/time_tracker/time_entries/_domain/_services/_time_entry.py +++ b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py @@ -1,8 +1,9 @@ +import typing + from time_tracker.time_entries._domain import TimeEntry, TimeEntriesDao class TimeEntryService: - def __init__(self, time_entry_dao: TimeEntriesDao): self.time_entry_dao = time_entry_dao @@ -14,3 +15,9 @@ def delete(self, id: int) -> TimeEntry: def update(self, time_entry_id: int, new_time_entry: dict) -> TimeEntry: return self.time_entry_dao.update(time_entry_id, new_time_entry) + + def get_by_id(self, id: int) -> TimeEntry: + return self.time_entry_dao.get_by_id(id) + + def get_all(self) -> typing.List[TimeEntry]: + return self.time_entry_dao.get_all() diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py index 4f0ac92e..fdd1258d 100644 --- a/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py +++ b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py @@ -2,3 +2,5 @@ from ._create_time_entry_use_case import CreateTimeEntryUseCase from ._delete_time_entry_use_case import DeleteTimeEntryUseCase from ._update_time_entry_use_case import UpdateTimeEntryUseCase +from ._get_time_entry_use_case import GetTimeEntriesUseCase +from ._get_time_entry_by_id_use_case import GetTimeEntryUseCase diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py new file mode 100644 index 00000000..410233e1 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_by_id_use_case.py @@ -0,0 +1,9 @@ +from time_tracker.time_entries._domain import TimeEntryService, TimeEntry + + +class GetTimeEntryUseCase: + def __init__(self, time_entry_service: TimeEntryService): + self.time_entry_service = time_entry_service + + def get_time_entry_by_id(self, id: int) -> TimeEntry: + return self.time_entry_service.get_by_id(id) diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py new file mode 100644 index 00000000..c7bd3f27 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_get_time_entry_use_case.py @@ -0,0 +1,11 @@ +import typing + +from time_tracker.time_entries._domain import TimeEntryService, TimeEntry + + +class GetTimeEntriesUseCase: + def __init__(self, time_entry_service: TimeEntryService): + self.time_entry_service = time_entry_service + + def get_time_entries(self) -> typing.List[TimeEntry]: + return self.time_entry_service.get_all() diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py index b999febe..76b56455 100644 --- a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/__init__.py @@ -1,2 +1,2 @@ # flake8: noqa -from ._time_entries_dao import TimeEntriesSQLDao +from ._time_entries_sql_dao import TimeEntriesSQLDao diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_sql_dao.py similarity index 85% rename from V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py rename to V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_sql_dao.py index 9c0740fa..9e7016d4 100644 --- a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_dao.py +++ b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/_time_entries_sql_dao.py @@ -1,6 +1,8 @@ import dataclasses +import typing import sqlalchemy +import sqlalchemy.sql as sql import time_tracker.time_entries._domain as domain from time_tracker._infrastructure import _db @@ -31,6 +33,19 @@ def __init__(self, database: _db.DB): extend_existing=True, ) + def get_by_id(self, time_entry_id: int) -> domain.TimeEntry: + query = sql.select(self.time_entry).where(self.time_entry.c.id == time_entry_id) + time_entry = self.db.get_session().execute(query).one_or_none() + return self.__create_time_entry_dto(dict(time_entry)) if time_entry else None + + def get_all(self) -> typing.List[domain.TimeEntry]: + query = sql.select(self.time_entry) + result = self.db.get_session().execute(query) + return [ + self.__create_time_entry_dto(dict(time_entry)) + for time_entry in result + ] + def create(self, time_entry_data: domain.TimeEntry) -> domain.TimeEntry: try: new_time_entry = time_entry_data.__dict__ diff --git a/V2/time_tracker/time_entries/interface.py b/V2/time_tracker/time_entries/interface.py index 7e1be4ef..8873b93d 100644 --- a/V2/time_tracker/time_entries/interface.py +++ b/V2/time_tracker/time_entries/interface.py @@ -2,3 +2,4 @@ from ._application import create_time_entry from ._application import delete_time_entry from ._application import update_time_entry +from ._application import get_time_entries