diff --git a/V2/source/entry_points/flask_api/__init__.py b/V2/source/entry_points/flask_api/__init__.py new file mode 100644 index 00000000..65fbcb89 --- /dev/null +++ b/V2/source/entry_points/flask_api/__init__.py @@ -0,0 +1,30 @@ +from flask import Flask +from flask_wtf.csrf import CSRFProtect +from flask_restplus import Namespace, Resource, Api +from http import HTTPStatus +from . import activities_endpoints + +csrf = CSRFProtect() + + +def create_app(test_config=None): + app = Flask(__name__) + csrf.init_app(app) + + api = Api( + app, + version='1.0', + title='Time Tracker API', + description='API for the TimeTracker project', + ) + + if test_config is not None: + app.config.from_mapping(test_config) + + activities_namespace = Namespace('activities', description='Endpoint for activities') + activities_namespace.route('/')(activities_endpoints.Activities) + activities_namespace.route('/')(activities_endpoints.Activity) + + api.add_namespace(activities_namespace) + + return app diff --git a/V2/source/entry_points/flask_api/activities_endpoints.py b/V2/source/entry_points/flask_api/activities_endpoints.py new file mode 100644 index 00000000..3dce2a6a --- /dev/null +++ b/V2/source/entry_points/flask_api/activities_endpoints.py @@ -0,0 +1,31 @@ +from V2.source.daos.activities_json_dao import ActivitiesJsonDao +from V2.source.services.activity_service import ActivityService +from V2.source import use_cases +from flask_restplus import Resource +from http import HTTPStatus + +JSON_PATH = './V2/source/activities_data.json' + + +class Activities(Resource): + def get(self): + activities = use_cases.GetActivitiesUseCase( + create_activity_service(JSON_PATH) + ) + return [activity.__dict__ for activity in activities.get_activities()] + + +class Activity(Resource): + def get(self, activity_id: str): + try: + activity = use_cases.GetActivityUseCase( + create_activity_service(JSON_PATH) + ) + return activity.get_activity_by_id(activity_id).__dict__ + except AttributeError: + return {'message': 'Activity not found'}, HTTPStatus.NOT_FOUND + + +def create_activity_service(path: str): + activity_json = ActivitiesJsonDao(path) + return ActivityService(activity_json) diff --git a/V2/source/use_cases/__init__.py b/V2/source/use_cases/__init__.py new file mode 100644 index 00000000..a937b03d --- /dev/null +++ b/V2/source/use_cases/__init__.py @@ -0,0 +1,2 @@ +from ._get_activities_use_case import GetActivitiesUseCase +from ._get_activity_by_id_use_case import GetActivityUseCase diff --git a/V2/source/use_cases/_get_activities_use_case.py b/V2/source/use_cases/_get_activities_use_case.py new file mode 100644 index 00000000..16bd937b --- /dev/null +++ b/V2/source/use_cases/_get_activities_use_case.py @@ -0,0 +1,11 @@ +from V2.source.services.activity_service import ActivityService +from V2.source.dtos.activity import Activity +import typing + + +class GetActivitiesUseCase: + def __init__(self, activity_service: ActivityService): + self.activity_service = activity_service + + def get_activities(self) -> typing.List[Activity]: + return self.activity_service.get_all() diff --git a/V2/source/use_cases/_get_activity_by_id_use_case.py b/V2/source/use_cases/_get_activity_by_id_use_case.py new file mode 100644 index 00000000..3f63b9df --- /dev/null +++ b/V2/source/use_cases/_get_activity_by_id_use_case.py @@ -0,0 +1,10 @@ +from V2.source.services.activity_service import ActivityService +from V2.source.dtos.activity import Activity + + +class GetActivityUseCase: + def __init__(self, activity_service: ActivityService): + self.activity_service = activity_service + + def get_activity_by_id(self, id: str) -> Activity: + return self.activity_service.get_by_id(id) diff --git a/V2/tests/api/flask/activity_endpoints_test.py b/V2/tests/api/flask/activity_endpoints_test.py new file mode 100644 index 00000000..9ead6c98 --- /dev/null +++ b/V2/tests/api/flask/activity_endpoints_test.py @@ -0,0 +1,86 @@ +from V2.source.entry_points.flask_api import create_app +import json +import pytest +import typing +from flask.testing import FlaskClient +from http import HTTPStatus +from faker import Faker +import shutil + + +@pytest.fixture +def client(): + app = create_app({'TESTING': True}) + with app.test_client() as client: + yield client + + +@pytest.fixture +def activities_json(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': None, + 'tenant_id': 'cc925a5d-9644-4a4f-8d99-0bee49aadd05', + }, + { + 'id': '94ec92e2-a500-4700-a9f6-e41eb7b5507c', + 'name': 'Management', + 'description': None, + 'deleted': '7cf6efe5-a221-4fe4-b94f-8945127a489a', + 'status': None, + '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) + + with open(json_file) as outfile: + activities_json = json.load(outfile) + + yield activities_json + shutil.rmtree(temporary_directory) + + +def test_test__activity_endpoint__returns_all_activities( + client: FlaskClient, activities_json: typing.List[dict] +): + response = client.get("/activities/") + json_data = json.loads(response.data) + + assert response.status_code == HTTPStatus.OK + assert json_data == activities_json + + +def test__activity_endpoint__returns_an_activity__when_activity_matches_its_id( + client: FlaskClient, activities_json: typing.List[dict] +): + response = client.get("/activities/%s" % activities_json[0]['id']) + json_data = json.loads(response.data) + + assert response.status_code == HTTPStatus.OK + assert json_data == activities_json[0] + + +def test__activity_endpoint__returns_a_not_found_status__when_no_activity_matches_its_id( + client: FlaskClient, +): + response = client.get("/activities/%s" % Faker().uuid4()) + json_data = json.loads(response.data) + + assert response.status_code == HTTPStatus.NOT_FOUND + assert json_data['message'] == 'Activity not found' diff --git a/V2/tests/daos/activities_json_dao_test.py b/V2/tests/integration/daos/activities_json_dao_test.py similarity index 100% rename from V2/tests/daos/activities_json_dao_test.py rename to V2/tests/integration/daos/activities_json_dao_test.py diff --git a/V2/tests/unit/entry_points/flask/activity_class_endpoint_test.py b/V2/tests/unit/entry_points/flask/activity_class_endpoint_test.py new file mode 100644 index 00000000..1ed41eeb --- /dev/null +++ b/V2/tests/unit/entry_points/flask/activity_class_endpoint_test.py @@ -0,0 +1,55 @@ +from V2.source.entry_points.flask_api.activities_endpoints import ( + Activities, + Activity, +) +from V2.source import use_cases +from V2.source.dtos.activity import Activity as ActivityDTO +from pytest_mock import MockFixture +from faker import Faker +from werkzeug.exceptions import NotFound + +fake = Faker() + +valid_id = fake.uuid4() + +fake_activity = { + "name": fake.company(), + "description": fake.paragraph(), + "tenant_id": fake.uuid4(), + "id": valid_id, + "deleted": fake.date(), + "status": fake.boolean(), +} +fake_activity_dto = ActivityDTO(**fake_activity) + + +def test__activities_class__uses_the_get_activities_use_case__to_retrieve_activities( + mocker: MockFixture, +): + mocker.patch.object( + use_cases.GetActivitiesUseCase, + 'get_activities', + return_value=[], + ) + + activities_class_endpoint = Activities() + activities = activities_class_endpoint.get() + + assert use_cases.GetActivitiesUseCase.get_activities.called + assert [] == activities + + +def test__activity_class__uses_the_get_activity_by_id_use_case__to_retrieve__an_activity( + mocker: MockFixture, +): + mocker.patch.object( + use_cases.GetActivityUseCase, + 'get_activity_by_id', + return_value=fake_activity_dto, + ) + + activity_class_endpoint = Activity() + activity = activity_class_endpoint.get(valid_id) + + assert use_cases.GetActivityUseCase.get_activity_by_id.called + assert fake_activity == activity diff --git a/V2/tests/services/activity_service_test.py b/V2/tests/unit/services/activity_service_test.py similarity index 100% rename from V2/tests/services/activity_service_test.py rename to V2/tests/unit/services/activity_service_test.py diff --git a/V2/tests/unit/use_cases/activities_use_case_test.py b/V2/tests/unit/use_cases/activities_use_case_test.py new file mode 100644 index 00000000..3cb5b664 --- /dev/null +++ b/V2/tests/unit/use_cases/activities_use_case_test.py @@ -0,0 +1,36 @@ +from V2.source.services.activity_service import ActivityService +from V2.source import use_cases +from pytest_mock import MockFixture +from faker import Faker + +fake = Faker() + + +def test__get_list_activities_function__uses_the_activities_service__to_retrieve_activities( + mocker: MockFixture, +): + expected_activities = mocker.Mock() + activity_service = mocker.Mock( + get_all=mocker.Mock(return_value=expected_activities) + ) + + activities_use_case = use_cases.GetActivitiesUseCase(activity_service) + actual_activities = activities_use_case.get_activities() + + assert activity_service.get_all.called + assert expected_activities == actual_activities + + +def test__get_activity_by_id_function__uses_the_activities_service__to_retrieve_activity( + mocker: MockFixture, +): + expected_activity = mocker.Mock() + activity_service = mocker.Mock( + get_by_id=mocker.Mock(return_value=expected_activity) + ) + + activity_use_case = use_cases.GetActivityUseCase(activity_service) + actual_activity = activity_use_case.get_activity_by_id(fake.uuid4()) + + assert activity_service.get_by_id.called + assert expected_activity == actual_activity diff --git a/requirements/time_tracker_api/prod.txt b/requirements/time_tracker_api/prod.txt index c7755c94..cba1f715 100644 --- a/requirements/time_tracker_api/prod.txt +++ b/requirements/time_tracker_api/prod.txt @@ -9,6 +9,7 @@ #Required by Flask Flask==1.1.1 +Flask-WTF==0.15.1 flake8==3.7.9 WSGIserver==1.3 Werkzeug==0.16.1