diff --git a/V2/create_activity/function.json b/V2/create_activity/function.json deleted file mode 100644 index ed3454a9..00000000 --- a/V2/create_activity/function.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "disabled": false, - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "route": "activities/", - "authLevel": "anonymous", - "methods": [ - "POST" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ], - "entryPoint": "create_activity", - "scriptFile": "../time_tracker/activities/interface.py" -} \ No newline at end of file diff --git a/V2/delete_activity/function.json b/V2/delete_activity/function.json deleted file mode 100644 index d51170fd..00000000 --- a/V2/delete_activity/function.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "disabled": false, - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "route": "activities/{id}", - "authLevel": "anonymous", - "methods": [ - "DELETE" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ], - "entryPoint": "delete_activity", - "scriptFile": "../time_tracker/activities/interface.py" -} \ No newline at end of file diff --git a/V2/delete_time_entry/function.json b/V2/delete_time_entry/function.json deleted file mode 100644 index 1c1c0720..00000000 --- a/V2/delete_time_entry/function.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "disabled": false, - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "route": "time-entries/{id}", - "authLevel": "anonymous", - "methods": [ - "DELETE" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ], - "entryPoint": "delete_time_entry", - "scriptFile": "../time_tracker/time_entries/interface.py" -} \ No newline at end of file diff --git a/V2/get_activities/function.json b/V2/get_activities/function.json deleted file mode 100644 index ee1efe53..00000000 --- a/V2/get_activities/function.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "disabled": false, - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "route": "activities/{id:?}", - "authLevel": "anonymous", - "methods": [ - "GET" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ], - "entryPoint": "get_activities", - "scriptFile": "../time_tracker/activities/interface.py" -} \ No newline at end of file diff --git a/V2/pyvenv.cfg b/V2/pyvenv.cfg new file mode 100644 index 00000000..853404e2 --- /dev/null +++ b/V2/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.8.10 diff --git a/V2/serverless.yml b/V2/serverless.yml index dc025f68..b8531b37 100644 --- a/V2/serverless.yml +++ b/V2/serverless.yml @@ -1,6 +1,6 @@ service: azure-time-tracker -frameworkVersion: "2" +frameworkVersion: '2' provider: name: azure @@ -23,18 +23,18 @@ plugins: package: patterns: - - "!env/**" - - "!.env/**" - - "!local.settings.json" - - "!.vscode/**" - - "!__pycache__/**" - - "!node_modules/**" - - "!.python_packages/**" - - "!.funcignore" - - "!package.json" - - "!package-lock.json" - - "!.gitignore" - - "!.git/**" + - '!env/**' + - '!.env/**' + - '!local.settings.json' + - '!.vscode/**' + - '!__pycache__/**' + - '!node_modules/**' + - '!.python_packages/**' + - '!.funcignore' + - '!package.json' + - '!package-lock.json' + - '!.gitignore' + - '!.git/**' functions: get_activities: @@ -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 @@ -77,6 +77,17 @@ functions: route: activities/ authLevel: anonymous + + create_time_entry: + handler: time_tracker/time_entries/interface.create_time_entry + events: + - http: true + x-azure-settings: + methods: + - POST + route: time-entries/ + authLevel: anonymous + delete_time_entry: handler: time_tracker/time_entries/interface.delete_time_entry events: @@ -85,4 +96,4 @@ functions: methods: - DELETE route: time-entries/{id} - authLevel: anonymous + authLevel: anonymous \ No newline at end of file diff --git a/V2/tests/api/azure/activity_azure_endpoints_test.py b/V2/tests/api/azure/activity_azure_endpoints_test.py index 9b2618a8..e428ecba 100644 --- a/V2/tests/api/azure/activity_azure_endpoints_test.py +++ b/V2/tests/api/azure/activity_azure_endpoints_test.py @@ -1,24 +1,12 @@ -import pytest import json from faker import Faker import azure.functions as func import time_tracker.activities._application._activities as azure_activities -import time_tracker.activities._infrastructure as infrastructure -from time_tracker._infrastructure import DB -from time_tracker.activities import _domain - -ACTIVITY_URL = '/api/activities/' -@pytest.fixture(name='insert_activity') -def _insert_activity() -> dict: - def _new_activity(activity: _domain.Activity, database: DB): - dao = infrastructure.ActivitiesSQLDao(database) - new_activity = dao.create(activity) - return new_activity.__dict__ - return _new_activity +ACTIVITY_URL = '/api/activities/' def test__activity_azure_endpoint__returns_all_activities( @@ -27,8 +15,8 @@ def test__activity_azure_endpoint__returns_all_activities( fake_database = create_fake_database existent_activities = [activity_factory(), activity_factory()] inserted_activities = [ - insert_activity(existent_activities[0], fake_database), - insert_activity(existent_activities[1], fake_database) + insert_activity(existent_activities[0], fake_database).__dict__, + insert_activity(existent_activities[1], fake_database).__dict__ ] azure_activities._get_activities.DATABASE = fake_database @@ -45,7 +33,7 @@ def test__activity_azure_endpoint__returns_an_activity__when_activity_matches_it ): fake_database = create_fake_database existent_activity = activity_factory() - inserted_activity = insert_activity(existent_activity, fake_database) + inserted_activity = insert_activity(existent_activity, fake_database).__dict__ azure_activities._get_activities.DATABASE = fake_database req = func.HttpRequest( @@ -67,7 +55,7 @@ def test__activity_azure_endpoint__returns_an_activity_with_inactive_status__whe ): fake_database = create_fake_database existent_activity = activity_factory() - inserted_activity = insert_activity(existent_activity, fake_database) + inserted_activity = insert_activity(existent_activity, fake_database).__dict__ azure_activities._delete_activity.DATABASE = fake_database req = func.HttpRequest( @@ -90,7 +78,7 @@ def test__update_activity_azure_endpoint__returns_an_activity__when_found_an_act ): fake_database = create_fake_database existent_activity = activity_factory() - inserted_activity = insert_activity(existent_activity, fake_database) + inserted_activity = insert_activity(existent_activity, fake_database).__dict__ azure_activities._update_activity.DATABASE = fake_database activity_body = {"description": Faker().sentence()} 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 70a70b35..4b8ff699 100644 --- a/V2/tests/api/azure/time_entry_azure_endpoints_test.py +++ b/V2/tests/api/azure/time_entry_azure_endpoints_test.py @@ -1,36 +1,54 @@ -from time_tracker.time_entries._application import _time_entries as time_entries +import pytest +import json import azure.functions as func -import json +import time_tracker.time_entries._application._time_entries as azure_time_entries +from time_tracker._infrastructure import DB +from time_tracker.time_entries import _domain as domain_time_entries +from time_tracker.time_entries import _infrastructure as infrastructure_time_entries TIME_ENTRY_URL = "/api/time-entries/" +@pytest.fixture(name='insert_time_entry') +def _insert_time_entry() -> domain_time_entries.TimeEntry: + def _new_time_entry(time_entry: domain_time_entries.TimeEntry, database: DB): + dao = infrastructure_time_entries.TimeEntriesSQLDao(database) + new_time_entry = dao.create(time_entry) + return new_time_entry + return _new_time_entry + + def test__delete_time_entries_azure_endpoint__returns_an_time_entry_with_true_deleted__when_its_id_is_found( - create_temp_time_entries, + create_fake_database, time_entry_factory, insert_time_entry, insert_activity, activity_factory, ): - time_entries_json, tmp_directory = create_temp_time_entries - time_entries.delete_time_entry.JSON_PATH = tmp_directory + db = create_fake_database + azure_time_entries._delete_time_entry.DATABASE = db + inserted_activity = insert_activity(activity_factory(), db).__dict__ + time_entry_body = time_entry_factory(activity_id=inserted_activity["id"], technologies="[jira,sql]") + inserted_time_entry = insert_time_entry(time_entry_body, db) + req = func.HttpRequest( - method="DELETE", + method='DELETE', body=None, url=TIME_ENTRY_URL, - route_params={"id": time_entries_json[0]["id"]}, + route_params={"id": inserted_time_entry.id}, ) - response = time_entries.delete_time_entry(req) + 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 time_entry_json_data["deleted"] is True + assert time_entry_json_data['deleted'] is True def test__delete_time_entries_azure_endpoint__returns_a_status_code_400__when_time_entry_recive_invalid_id( - create_temp_time_entries, + create_fake_database, ): - tmp_directory = create_temp_time_entries - time_entries.delete_time_entry.JSON_PATH = tmp_directory + db = create_fake_database + azure_time_entries._delete_time_entry.DATABASE = db + req = func.HttpRequest( method="DELETE", body=None, @@ -38,7 +56,30 @@ def test__delete_time_entries_azure_endpoint__returns_a_status_code_400__when_ti route_params={"id": "invalid id"}, ) - response = time_entries.delete_time_entry(req) + response = azure_time_entries._delete_time_entry.delete_time_entry(req) assert response.status_code == 400 assert response.get_body() == b'Invalid Format ID' + + +def test__time_entry_azure_endpoint__creates_an_time_entry__when_time_entry_has_all_attributes( + create_fake_database, time_entry_factory, activity_factory, insert_activity +): + db = create_fake_database + inserted_activity = insert_activity(activity_factory(), db).__dict__ + time_entry_body = time_entry_factory(activity_id=inserted_activity["id"], technologies="[jira,sql]").__dict__ + + azure_time_entries._create_time_entry._DATABASE = db + body = json.dumps(time_entry_body).encode("utf-8") + req = func.HttpRequest( + method='POST', + body=body, + url=TIME_ENTRY_URL, + ) + + response = azure_time_entries._create_time_entry.create_time_entry(req) + time_entry_json_data = json.loads(response.get_body()) + time_entry_body['id'] = time_entry_json_data['id'] + + assert response.status_code == 201 + assert time_entry_json_data == time_entry_body diff --git a/V2/tests/conftest.py b/V2/tests/conftest.py index cafb2355..678d625f 100644 --- a/V2/tests/conftest.py +++ b/V2/tests/conftest.py @@ -1,8 +1,3 @@ # flake8: noqa -<<<<<<< HEAD -from fixtures import _activity_factory, _create_fake_dao, _create_fake_database -======= -from fixtures import create_temp_activities -from fixtures import create_temp_time_entries -from fixtures import _time_entry_factory ->>>>>>> 42cfc46... feat: TT-401 validated request create time entry +from fixtures import _activity_factory, _create_fake_database, _insert_activity +from fixtures import _time_entry_factory \ No newline at end of file diff --git a/V2/tests/fixtures.py b/V2/tests/fixtures.py index 142de8af..7eaaa38c 100644 --- a/V2/tests/fixtures.py +++ b/V2/tests/fixtures.py @@ -1,125 +1,54 @@ import pytest -import json -import pytest -import shutil from faker import Faker -import time_tracker.activities._domain as domain -import time_tracker.activities._infrastructure as infrastructure +import time_tracker.activities._domain as domain_activities +import time_tracker.time_entries._domain as domain_time_entries +import time_tracker.activities._infrastructure as infrastructure_activities from time_tracker._infrastructure import DB -from time_tracker.time_entries._domain import TimeEntry -@pytest.fixture(name="activity_factory") -def _activity_factory() -> domain.Activity: +@pytest.fixture(name='activity_factory') +def _activity_factory() -> domain_activities.Activity: def _make_activity( name: str = Faker().name(), description: str = Faker().sentence(), deleted: bool = False, status: int = 1, ): - activity = domain.Activity( - id=None, name=name, description=description, deleted=deleted, status=status - ) + activity = domain_activities.Activity( + id=None, + name=name, + description=description, + deleted=deleted, + status=status + ) return activity return _make_activity -@pytest.fixture(name="create_fake_dao") -def _create_fake_dao() -> domain.ActivitiesDao: - db_fake = DB("sqlite:///:memory:") - dao = infrastructure.ActivitiesSQLDao(db_fake) - return dao - - -@pytest.fixture(name="create_fake_database") -def _create_fake_database() -> domain.ActivitiesDao: - db_fake = DB("sqlite:///:memory:") +@pytest.fixture(name='create_fake_database') +def _create_fake_database() -> DB: + db_fake = DB('sqlite:///:memory:') return db_fake -@pytest.fixture -def create_temp_activities(tmpdir_factory): - temporary_directory = tmpdir_factory.mktemp("tmp") - json_file = temporary_directory.join("activities.json") - activities = [ - { - "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072b", - "name": "Development", - "description": "Development", - "deleted": "b4327ba6-9f96-49ee-a9ac-3c1edf525172", - "status": "active", - "tenant_id": "cc925a5d-9644-4a4f-8d99-0bee49aadd05", - }, - { - "id": "94ec92e2-a500-4700-a9f6-e41eb7b5507c", - "name": "Management", - "description": "Description of management", - "deleted": "7cf6efe5-a221-4fe4-b94f-8945127a489a", - "status": "active", - "tenant_id": "cc925a5d-9644-4a4f-8d99-0bee49aadd05", - }, - { - "id": "d45c770a-b1a0-4bd8-a713-22c01a23e41b", - "name": "Operations", - "description": "Operation activities performed.", - "deleted": "7cf6efe5-a221-4fe4-b94f-8945127a489a", - "status": "active", - "tenant_id": "cc925a5d-9644-4a4f-8d99-0bee49aadd05", - }, - ] - - with open(json_file, "w") as outfile: - json.dump(activities, outfile) - - yield activities, json_file - shutil.rmtree(temporary_directory) - - -@pytest.fixture -def create_temp_time_entries(tmpdir_factory): - temporary_directory = tmpdir_factory.mktemp("tmp") - json_file = temporary_directory.join("time_entries.json") - time_entries = [ - { - "id": 1, - "start_date": Faker().date(), - "owner_id": Faker().random_int(), - "description": Faker().sentence(), - "activity_id": Faker().random_int(), - "uri": Faker().domain_name(), - "technologies": ["jira", "git"], - "end_date": Faker().date(), - "deleted": Faker().random_int(), - "timezone_offset": "300", - "project_id": Faker().random_int(), - } - ] - - with open(json_file, "w") as outfile: - json.dump(time_entries, outfile) - - yield time_entries, json_file - shutil.rmtree(temporary_directory) - - -@pytest.fixture(name="time_entry_factory") -def _time_entry_factory() -> TimeEntry: +@pytest.fixture(name='time_entry_factory') +def _time_entry_factory() -> domain_time_entries.TimeEntry: def _make_time_entry( id=Faker().random_int(), - start_date=Faker().date(), + start_date=str(Faker().date_time()), owner_id=Faker().random_int(), description=Faker().sentence(), activity_id=Faker().random_int(), uri=Faker().domain_name(), technologies=["jira", "git"], - end_date=Faker().date(), + end_date=str(Faker().date_time()), deleted=False, timezone_offset="300", project_id=Faker().random_int(), ): - time_entry = TimeEntry( + time_entry = domain_time_entries.TimeEntry( id=id, start_date=start_date, owner_id=owner_id, @@ -133,5 +62,13 @@ def _make_time_entry( project_id=project_id, ) return time_entry - return _make_time_entry + + +@pytest.fixture(name='insert_activity') +def _insert_activity() -> domain_activities.Activity: + def _new_activity(activity: domain_activities.Activity, database: DB): + dao = infrastructure_activities.ActivitiesSQLDao(database) + new_activity = dao.create(activity) + return new_activity + return _new_activity diff --git a/V2/tests/integration/daos/activities_sql_dao_test.py b/V2/tests/integration/daos/activities_sql_dao_test.py index 25f62500..0f0170af 100644 --- a/V2/tests/integration/daos/activities_sql_dao_test.py +++ b/V2/tests/integration/daos/activities_sql_dao_test.py @@ -7,12 +7,11 @@ from time_tracker._infrastructure import DB -@pytest.fixture(name='insert_activity') -def _insert_activity() -> domain.Activity: - def _new_activity(activity: domain.Activity, dao: domain.ActivitiesDao): - new_activity = dao.create(activity) - return new_activity - return _new_activity +@pytest.fixture(name='create_fake_dao') +def _create_fake_dao() -> domain.ActivitiesDao: + db_fake = DB('sqlite:///:memory:') + dao = infrastructure.ActivitiesSQLDao(db_fake) + return dao @pytest.fixture(name='clean_database', autouse=True) @@ -41,7 +40,7 @@ def test_update__returns_an_update_activity__when_an_activity_matching_its_id_is ): dao = create_fake_dao existent_activity = activity_factory() - inserted_activity = insert_activity(existent_activity, dao) + inserted_activity = insert_activity(existent_activity, dao.db) expected_description = Faker().sentence() updated_activity = dao.update(inserted_activity.id, None, expected_description, None, None) @@ -68,8 +67,8 @@ def test__get_all__returns_a_list_of_activity_dto_objects__when_one_or_more_acti dao = create_fake_dao existent_activities = [activity_factory(), activity_factory()] inserted_activities = [ - insert_activity(existent_activities[0], dao), - insert_activity(existent_activities[1], dao) + insert_activity(existent_activities[0], dao.db), + insert_activity(existent_activities[1], dao.db) ] activities = dao.get_all() @@ -83,7 +82,7 @@ def test_get_by_id__returns_an_activity_dto__when_found_one_activity_that_matche ): dao = create_fake_dao existent_activity = activity_factory() - inserted_activity = insert_activity(existent_activity, dao) + inserted_activity = insert_activity(existent_activity, dao.db) activity = dao.get_by_id(inserted_activity.id) @@ -117,7 +116,7 @@ def test_delete__returns_an_activity_with_inactive_status__when_an_activity_matc ): dao = create_fake_dao existent_activity = activity_factory() - inserted_activity = insert_activity(existent_activity, dao) + inserted_activity = insert_activity(existent_activity, dao.db) activity = dao.delete(inserted_activity.id) diff --git a/V2/tests/integration/daos/time_entries_dao.test.py b/V2/tests/integration/daos/time_entries_dao.test.py deleted file mode 100644 index 49191e62..00000000 --- a/V2/tests/integration/daos/time_entries_dao.test.py +++ /dev/null @@ -1,45 +0,0 @@ -import json -import pytest -import typing - -from faker import Faker - -from time_tracker.time_entries._infrastructure import TimeEntriesJsonDao -from time_tracker.time_entries._domain import TimeEntry - - -@pytest.fixture(name="create_fake_time_entries") -def _create_fake_time_entries(mocker) -> typing.List[TimeEntry]: - def _creator(time_entries): - read_data = json.dumps(time_entries) - mocker.patch("builtins.open", mocker.mock_open(read_data=read_data)) - return [TimeEntry(**time_entry) for time_entry in time_entries] - - return _creator - - -def test_delete__returns_an_time_entry_with_true_deleted__when_an_time_entry_matching_its_id_is_found( - create_fake_time_entries, time_entry_factory -): - time_entries_json_dao = TimeEntriesJsonDao(Faker().file_path()) - time_entries = create_fake_time_entries( - [ - time_entry_factory().__dict__ - ] - ) - - time_entry_dto = time_entries[0] - result = time_entries_json_dao.delete(time_entry_dto.id) - - assert result.deleted is True - - -def test_delete__returns_none__when_no_time_entry_matching_its_id_is_found( - create_fake_time_entries, -): - time_entries_json_dao = TimeEntriesJsonDao(Faker().file_path()) - create_fake_time_entries([]) - - result = time_entries_json_dao.delete(Faker().pyint()) - - assert result is None diff --git a/V2/tests/integration/daos/time_entries_dao_test.py b/V2/tests/integration/daos/time_entries_dao_test.py new file mode 100644 index 00000000..9c062a8a --- /dev/null +++ b/V2/tests/integration/daos/time_entries_dao_test.py @@ -0,0 +1,70 @@ +import pytest +from faker import Faker + +import time_tracker.time_entries._domain as domain +import time_tracker.time_entries._infrastructure as infrastructure +from time_tracker._infrastructure import DB + + +@pytest.fixture(name='create_fake_dao') +def _create_fake_dao() -> domain.TimeEntriesDao: + db_fake = DB('sqlite:///:memory:') + dao = infrastructure.TimeEntriesSQLDao(db_fake) + return dao + + +@pytest.fixture(name='clean_database', autouse=True) +def _clean_database(): + yield + db_fake = DB('sqlite:///:memory:') + dao = infrastructure.TimeEntriesSQLDao(db_fake) + query = dao.time_entry.delete() + dao.db.get_session().execute(query) + + +def test__time_entry__returns_a_time_entry_dto__when_saves_correctly_with_sql_database( + time_entry_factory, create_fake_dao, insert_activity, activity_factory +): + dao = create_fake_dao + inserted_activity = insert_activity(activity_factory(), dao.db) + + existent_time_entry = time_entry_factory(activity_id=inserted_activity.id, technologies="[jira,sql]") + + inserted_time_entry = dao.create(existent_time_entry) + + assert isinstance(inserted_time_entry, domain.TimeEntry) + assert inserted_time_entry == existent_time_entry + + +def test__time_entry__returns_None__when_not_saves_correctly( + time_entry_factory, create_fake_dao, +): + dao = create_fake_dao + existent_time_entry = time_entry_factory(activity_id=1203, technologies="[jira,sql]") + + inserted_time_entry = dao.create(existent_time_entry) + + assert inserted_time_entry is None + + +def test_delete__returns_an_time_entry_with_true_deleted__when_an_time_entry_matching_its_id_is_found( + create_fake_dao, time_entry_factory, insert_activity, activity_factory +): + dao = create_fake_dao + inserted_activity = insert_activity(activity_factory(), dao.db) + existent_time_entry = time_entry_factory(activity_id=inserted_activity.id, technologies="[jira,sql]") + inserted_time_entry = dao.create(existent_time_entry) + + result = dao.delete(inserted_time_entry.id) + + assert result.deleted is True + + +def test_delete__returns_none__when_no_time_entry_matching_its_id_is_found( + create_fake_dao, +): + dao = create_fake_dao + + result = dao.delete(Faker().pyint()) + + assert result 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 d13af483..7e04ea73 100644 --- a/V2/tests/unit/services/time_entry_service_test.py +++ b/V2/tests/unit/services/time_entry_service_test.py @@ -16,3 +16,16 @@ def test__delete_time_entry__uses_the_time_entry_dao__to_change_time_entry_delet assert time_entry_dao.delete.called assert expected_time_entry == deleted_time_entry + + +def test__create_time_entries__uses_the_time_entry_dao__to_create_an_time_entry(mocker, time_entry_factory): + expected_time_entry = mocker.Mock() + time_entry_dao = mocker.Mock( + create=mocker.Mock(return_value=expected_time_entry) + ) + time_entry_service = TimeEntryService(time_entry_dao) + + actual_time_entry = time_entry_service.create(time_entry_factory()) + + assert time_entry_dao.create.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 d5095c4f..0ab6345b 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 @@ -15,3 +15,18 @@ def test__delete_time_entry_function__uses_the_time_entry_service__to_change_tim assert time_entry_service.delete.called assert expected_time_entry == deleted_time_entry + + +def test__create_time_entry_function__uses_the_time_entries_service__to_create_time_entry( + mocker: MockFixture, time_entry_factory +): + expected_time_entry = mocker.Mock() + time_entry_service = mocker.Mock( + create=mocker.Mock(return_value=expected_time_entry) + ) + + time_entry_use_case = _use_cases.CreateTimeEntryUseCase(time_entry_service) + actual_time_entry = time_entry_use_case.create_time_entry(time_entry_factory()) + + assert time_entry_service.create.called + assert expected_time_entry == actual_time_entry diff --git a/V2/time_tracker/_infrastructure/_db.py b/V2/time_tracker/_infrastructure/_db.py index 8fe5cef1..0f7dae1a 100644 --- a/V2/time_tracker/_infrastructure/_db.py +++ b/V2/time_tracker/_infrastructure/_db.py @@ -2,6 +2,8 @@ from . import _config +_TEST_DIALECT = "sqlite" + class DB(): config = _config.load_config() @@ -14,7 +16,10 @@ def __init__(self, conn_string: str = conn_string): self.engine = sqlalchemy.create_engine(conn_string) def get_session(self): + + self.metadata.create_all(self.engine) if self.connection is None: - self.metadata.create_all(self.engine) self.connection = self.engine.connect() + if self.engine.dialect.name == _TEST_DIALECT: + self.connection.execute("pragma foreign_keys=ON") return self.connection diff --git a/V2/time_tracker/time_entries/_application/__init__.py b/V2/time_tracker/time_entries/_application/__init__.py index 2f7cebc5..8b818924 100644 --- a/V2/time_tracker/time_entries/_application/__init__.py +++ b/V2/time_tracker/time_entries/_application/__init__.py @@ -1,2 +1,3 @@ # flake8: noqa -from ._time_entries import delete_time_entry \ No newline at end of file +from ._time_entries import delete_time_entry +from ._time_entries import create_time_entry 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 7203d16d..a1ba947d 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/__init__.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/__init__.py @@ -1,2 +1,3 @@ # flake8: noqa -from ._delete_time_entry import delete_time_entry \ No newline at end of file +from ._delete_time_entry import delete_time_entry +from ._create_time_entry import create_time_entry diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py b/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py new file mode 100644 index 00000000..938eb50e --- /dev/null +++ b/V2/time_tracker/time_entries/_application/_time_entries/_create_time_entry.py @@ -0,0 +1,65 @@ +import dataclasses +import json +import typing + +import azure.functions as func + +from ... import _domain +from ... import _infrastructure +from time_tracker._infrastructure import DB + +_DATABASE = DB() + + +def create_time_entry(req: func.HttpRequest) -> func.HttpResponse: + + time_entry_dao = _infrastructure.TimeEntriesSQLDao(_DATABASE) + time_entry_service = _domain.TimeEntryService(time_entry_dao) + use_case = _domain._use_cases.CreateTimeEntryUseCase(time_entry_service) + + time_entry_data = req.get_json() + + validation_errors = _validate_time_entry(time_entry_data) + if validation_errors: + return func.HttpResponse( + body=json.dumps(validation_errors), status_code=400, mimetype="application/json" + ) + + time_entry_to_create = _domain.TimeEntry( + id=None, + start_date=time_entry_data["start_date"], + owner_id=time_entry_data["owner_id"], + description=time_entry_data["description"], + activity_id=time_entry_data["activity_id"], + uri=time_entry_data["uri"], + technologies=time_entry_data["technologies"], + end_date=time_entry_data["end_date"], + deleted=False, + timezone_offset=time_entry_data["timezone_offset"], + project_id=time_entry_data["project_id"] + ) + + created_time_entry = use_case.create_time_entry(time_entry_to_create) + + if not created_time_entry: + return func.HttpResponse( + body=json.dumps({'error': 'time_entry could not be created'}), + status_code=500, + mimetype="application/json" + ) + + return func.HttpResponse( + body=json.dumps(created_time_entry.__dict__), + status_code=201, + mimetype="application/json" + ) + + +def _validate_time_entry(time_entry_data: dict) -> typing.List[str]: + time_entry_fields = [field.name for field in dataclasses.fields(_domain.TimeEntry)] + time_entry_fields.pop(8) + missing_keys = [field for field in time_entry_fields if field not in time_entry_data] + return [ + f'The {missing_key} key is missing in the input data' + for missing_key in missing_keys + ] diff --git a/V2/time_tracker/time_entries/_application/_time_entries/_delete_time_entry.py b/V2/time_tracker/time_entries/_application/_time_entries/_delete_time_entry.py index 5664b1e6..8a4be18c 100644 --- a/V2/time_tracker/time_entries/_application/_time_entries/_delete_time_entry.py +++ b/V2/time_tracker/time_entries/_application/_time_entries/_delete_time_entry.py @@ -4,37 +4,35 @@ from ... import _domain from ... import _infrastructure +from time_tracker._infrastructure import DB -_JSON_PATH = ( - 'time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json' -) +DATABASE = DB() def delete_time_entry(req: func.HttpRequest) -> func.HttpResponse: - time_entry_dao = _infrastructure.TimeEntriesJsonDao(_JSON_PATH) + time_entry_dao = _infrastructure.TimeEntriesSQLDao(DATABASE) time_entry_service = _domain.TimeEntryService(time_entry_dao) use_case = _domain._use_cases.DeleteTimeEntryUseCase(time_entry_service) try: time_entry_id = int(req.route_params.get("id")) - deleted_time_entry = use_case.delete_time_entry(time_entry_id) if not deleted_time_entry: return func.HttpResponse( - body='Not found', - status_code=404, - mimetype="application/json" + body="Not found", + status_code=404, + mimetype="application/json" ) return func.HttpResponse( - body=json.dumps(deleted_time_entry.__dict__), - status_code=200, - mimetype="application/json" + body=json.dumps(deleted_time_entry.__dict__, default=str), + status_code=200, + mimetype="application/json", ) except ValueError: return func.HttpResponse( - body=b'Invalid Format ID', + body=b"Invalid Format ID", status_code=400, mimetype="application/json" ) diff --git a/V2/time_tracker/time_entries/_domain/__init__.py b/V2/time_tracker/time_entries/_domain/__init__.py index 3baaf4f0..f874a399 100644 --- a/V2/time_tracker/time_entries/_domain/__init__.py +++ b/V2/time_tracker/time_entries/_domain/__init__.py @@ -3,5 +3,6 @@ from ._persistence_contracts import TimeEntriesDao from ._services import TimeEntryService from ._use_cases import ( - DeleteTimeEntryUseCase + DeleteTimeEntryUseCase, + CreateTimeEntryUseCase, ) \ No newline at end of file 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 ea02f002..e7d94608 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 @@ -4,6 +4,10 @@ class TimeEntriesDao(abc.ABC): + @abc.abstractmethod + def create(self, time_entry_data: TimeEntry) -> TimeEntry: + pass + @abc.abstractmethod def delete(self, id: int) -> 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 1b7202b0..94474f3a 100644 --- a/V2/time_tracker/time_entries/_domain/_services/_time_entry.py +++ b/V2/time_tracker/time_entries/_domain/_services/_time_entry.py @@ -8,3 +8,6 @@ def __init__(self, time_entry_dao: TimeEntriesDao): def delete(self, time_entry_id: int) -> TimeEntry: return self.time_entry_dao.delete(time_entry_id) + + def create(self, time_entry_data: TimeEntry) -> TimeEntry: + return self.time_entry_dao.create(time_entry_data) 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 62a40986..b9efa59d 100644 --- a/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py +++ b/V2/time_tracker/time_entries/_domain/_use_cases/__init__.py @@ -1,2 +1,3 @@ # flake8: noqa from ._delete_time_entry_use_case import DeleteTimeEntryUseCase +from ._create_time_entry_use_case import CreateTimeEntryUseCase diff --git a/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py b/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py new file mode 100644 index 00000000..f2258468 --- /dev/null +++ b/V2/time_tracker/time_entries/_domain/_use_cases/_create_time_entry_use_case.py @@ -0,0 +1,10 @@ +from time_tracker.time_entries._domain import TimeEntry, TimeEntryService + + +class CreateTimeEntryUseCase: + + def __init__(self, time_entry_service: TimeEntryService): + self.time_entry_service = time_entry_service + + def create_time_entry(self, time_entry_data: TimeEntry) -> TimeEntry: + return self.time_entry_service.create(time_entry_data) diff --git a/V2/time_tracker/time_entries/_infrastructure/__init__.py b/V2/time_tracker/time_entries/_infrastructure/__init__.py index 0e90c78a..1c7a7d6d 100644 --- a/V2/time_tracker/time_entries/_infrastructure/__init__.py +++ b/V2/time_tracker/time_entries/_infrastructure/__init__.py @@ -1,2 +1,2 @@ # flake8: noqa -from ._data_persistence import TimeEntriesJsonDao +from ._data_persistence import TimeEntriesSQLDao 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 91ebd7cf..097a2d5b 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 TimeEntriesJsonDao +from ._time_entries_dao import TimeEntriesSQLDao \ No newline at end of file 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_dao.py index e5a74516..b52ab725 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_dao.py @@ -1,58 +1,60 @@ -import json import dataclasses -import typing +import sqlalchemy +import sqlalchemy.sql as sql from time_tracker.time_entries._domain import TimeEntriesDao, TimeEntry +import time_tracker.time_entries._domain as domain +from time_tracker._infrastructure import _db + + +class TimeEntriesSQLDao(TimeEntriesDao): + def __init__(self, database: _db.DB): + self.time_entry_keys = [field.name for field in dataclasses.fields(domain.TimeEntry)] + self.db = database + self.time_entry = sqlalchemy.Table( + 'time_entry', + self.db.metadata, + sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True, autoincrement=True), + sqlalchemy.Column('start_date', sqlalchemy.DateTime().with_variant(sqlalchemy.String, "sqlite")), + sqlalchemy.Column('owner_id', sqlalchemy.Integer), + sqlalchemy.Column('description', sqlalchemy.String), + sqlalchemy.Column('activity_id', sqlalchemy.Integer, sqlalchemy.ForeignKey('activity.id')), + sqlalchemy.Column('uri', sqlalchemy.String), + sqlalchemy.Column( + 'technologies', + sqlalchemy.ARRAY(sqlalchemy.String).with_variant(sqlalchemy.String, "sqlite") + ), + sqlalchemy.Column('end_date', sqlalchemy.DateTime().with_variant(sqlalchemy.String, "sqlite")), + sqlalchemy.Column('deleted', sqlalchemy.Boolean), + sqlalchemy.Column('timezone_offset', sqlalchemy.String), + sqlalchemy.Column('project_id', sqlalchemy.Integer), + extend_existing=True, + ) + def __create_time_entry_dto(self, time_entry: dict) -> TimeEntry: + time_entry = {key: time_entry.get(key) for key in self.time_entry_keys} + return TimeEntry(**time_entry) -class TimeEntriesJsonDao(TimeEntriesDao): - - def __init__(self, json_data_file_path: str): - self.json_data_file_path = json_data_file_path - self.time_entry_key = [field.name for field in dataclasses.fields(TimeEntry)] - - def __get_time_entries_from_file(self) -> typing.List[dict]: + def create(self, time_entry_data: domain.TimeEntry) -> domain.TimeEntry: try: - file = open(self.json_data_file_path) - time_entries = json.load(file) - file.close() - - return time_entries + new_time_entry = time_entry_data.__dict__ + new_time_entry.pop('id', None) - except FileNotFoundError: - return [] + query = self.time_entry.insert().values(new_time_entry).return_defaults() + time_entry = self.db.get_session().execute(query) + new_time_entry.update({"id": time_entry.inserted_primary_key[0]}) + return self.__create_time_entry_dto(new_time_entry) - def __create_time_entry_dto(self, time_entry: dict) -> TimeEntry: - time_entry = {key: time_entry.get(key) for key in self.time_entry_key} - return TimeEntry(**time_entry) + except sqlalchemy.exc.SQLAlchemyError: + return None def delete(self, time_entry_id: int) -> TimeEntry: - time_entry = { - time_entry.get('id'): time_entry - for time_entry in self.__get_time_entries_from_file() - }.get(int(time_entry_id)) - - if time_entry: - time_entry_deleted = {**time_entry, 'deleted': True} - - time_entries_updated = list( - map( - lambda time_entry: time_entry - if time_entry.get('id') != time_entry_id - else time_entry_deleted, - self.__get_time_entries_from_file(), - ) - ) - - try: - file = open(self.json_data_file_path, 'w') - json.dump(time_entries_updated, file) - file.close() - - return self.__create_time_entry_dto(time_entry_deleted) - - except FileNotFoundError: - return None - - else: - return None + query = ( + self.time_entry.update() + .where(self.time_entry.c.id == time_entry_id) + .values({"deleted": True}) + ) + self.db.get_session().execute(query) + query_deleted_time_entry = sql.select(self.time_entry).where(self.time_entry.c.id == time_entry_id) + time_entry = self.db.get_session().execute(query_deleted_time_entry).one_or_none() + return self.__create_time_entry_dto(dict(time_entry)) if time_entry else None diff --git a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json b/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json deleted file mode 100644 index 4749ceda..00000000 --- a/V2/time_tracker/time_entries/_infrastructure/_data_persistence/time_entries_data.json +++ /dev/null @@ -1 +0,0 @@ -[{"id": 1, "start_date": "12/07/2021", "owner_id": 2, "description": "Review system of Time Tracker", "activity_id": 2, "uri": "http://time_tracker.com", "technologies": ["git", "jira", "python"], "end_date": "12/07/2021", "deleted": false, "timezone_offset": "300", "project_id": 1}, {"id": 2, "start_date": "12/07/2021", "owner_id": 2, "description": "Review system of doors in the ioet office Loja, change the current raspberry pi ", "activity_id": 2, "uri": null, "technologies": ["git", "jira", "python"], "end_date": "12/07/2021", "deleted": true, "timezone_offset": "300", "project_id": 1}] \ No newline at end of file diff --git a/V2/time_tracker/time_entries/interface.py b/V2/time_tracker/time_entries/interface.py index 22e1c166..ef5ec636 100644 --- a/V2/time_tracker/time_entries/interface.py +++ b/V2/time_tracker/time_entries/interface.py @@ -1,2 +1,3 @@ # flake8: noqa -from ._application import delete_time_entry \ No newline at end of file +from ._application import delete_time_entry +from ._application import create_time_entry diff --git a/V2/update_activity/function.json b/V2/update_activity/function.json deleted file mode 100644 index 97c9fb49..00000000 --- a/V2/update_activity/function.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "disabled": false, - "bindings": [ - { - "type": "httpTrigger", - "direction": "in", - "name": "req", - "route": "activities/{id}", - "authLevel": "anonymous", - "methods": [ - "PUT" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - } - ], - "entryPoint": "update_activity", - "scriptFile": "../time_tracker/activities/interface.py" -} \ No newline at end of file