Skip to content

Commit 2c6e148

Browse files
JosueObAndrés Soto
andauthored
TT-352 create v2 read activites flask endpoint (#324)
* feat: TT-353 Create V2 Activities DAO * refactor: TT-353 Solving code smells from SonarCloud * refactor: TT-353 Solving duplicated literal * refactor: TT-353 Add type of argument and return type to functions * refactor: TT-353 Solving comments from PR * refactor: TT-353 Solving Sonarcloud code smell * refactor: TT-353 Changing variable names and tests * feat: TT-352 Create entry point and use case to get activities * feat: TT-352 Create entry point and use case to get activity * refactor: TT-352 use list comprehensions * refactor: TT-352 standarization flask_api directory * refactor: TT-353 Solving requested changes on PR * refactor: TT-352 use_cases and entry_points improvements to read activities * test: TT-352 Unit test of activity use cases * test: TT-352 entry_points and use_cases for activities complete testing * code-smell: TT-352 fixing code-smell * build: TT-352 implementation of CSRF Protection using Flask-WTF * refactor: TT-352 improving use_cases, endpoitns and test to get activitivies from JSON file * test: TT-352 improved testing of activity use cases * refactor: TT-352 improvement of endpoint testing for obtaining activities * refactor: TT-352 refactoring of the use case and endpoint to obtain activities * refactor: TT-352 refactoring of use cases and enpoint to obtain activities * test: TT-352 refactoring of use cases and enpoint to obtain activities * refactor: TT-352 refactoring of activity endpoints Co-authored-by: Andrés Soto <[email protected]>
1 parent c0b51c9 commit 2c6e148

File tree

11 files changed

+262
-0
lines changed

11 files changed

+262
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from flask import Flask
2+
from flask_wtf.csrf import CSRFProtect
3+
from flask_restplus import Namespace, Resource, Api
4+
from http import HTTPStatus
5+
from . import activities_endpoints
6+
7+
csrf = CSRFProtect()
8+
9+
10+
def create_app(test_config=None):
11+
app = Flask(__name__)
12+
csrf.init_app(app)
13+
14+
api = Api(
15+
app,
16+
version='1.0',
17+
title='Time Tracker API',
18+
description='API for the TimeTracker project',
19+
)
20+
21+
if test_config is not None:
22+
app.config.from_mapping(test_config)
23+
24+
activities_namespace = Namespace('activities', description='Endpoint for activities')
25+
activities_namespace.route('/')(activities_endpoints.Activities)
26+
activities_namespace.route('/<string:activity_id>')(activities_endpoints.Activity)
27+
28+
api.add_namespace(activities_namespace)
29+
30+
return app
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from V2.source.daos.activities_json_dao import ActivitiesJsonDao
2+
from V2.source.services.activity_service import ActivityService
3+
from V2.source import use_cases
4+
from flask_restplus import Resource
5+
from http import HTTPStatus
6+
7+
JSON_PATH = './V2/source/activities_data.json'
8+
9+
10+
class Activities(Resource):
11+
def get(self):
12+
activities = use_cases.GetActivitiesUseCase(
13+
create_activity_service(JSON_PATH)
14+
)
15+
return [activity.__dict__ for activity in activities.get_activities()]
16+
17+
18+
class Activity(Resource):
19+
def get(self, activity_id: str):
20+
try:
21+
activity = use_cases.GetActivityUseCase(
22+
create_activity_service(JSON_PATH)
23+
)
24+
return activity.get_activity_by_id(activity_id).__dict__
25+
except AttributeError:
26+
return {'message': 'Activity not found'}, HTTPStatus.NOT_FOUND
27+
28+
29+
def create_activity_service(path: str):
30+
activity_json = ActivitiesJsonDao(path)
31+
return ActivityService(activity_json)

V2/source/use_cases/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from ._get_activities_use_case import GetActivitiesUseCase
2+
from ._get_activity_by_id_use_case import GetActivityUseCase
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from V2.source.services.activity_service import ActivityService
2+
from V2.source.dtos.activity import Activity
3+
import typing
4+
5+
6+
class GetActivitiesUseCase:
7+
def __init__(self, activity_service: ActivityService):
8+
self.activity_service = activity_service
9+
10+
def get_activities(self) -> typing.List[Activity]:
11+
return self.activity_service.get_all()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from V2.source.services.activity_service import ActivityService
2+
from V2.source.dtos.activity import Activity
3+
4+
5+
class GetActivityUseCase:
6+
def __init__(self, activity_service: ActivityService):
7+
self.activity_service = activity_service
8+
9+
def get_activity_by_id(self, id: str) -> Activity:
10+
return self.activity_service.get_by_id(id)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from V2.source.entry_points.flask_api import create_app
2+
import json
3+
import pytest
4+
import typing
5+
from flask.testing import FlaskClient
6+
from http import HTTPStatus
7+
from faker import Faker
8+
import shutil
9+
10+
11+
@pytest.fixture
12+
def client():
13+
app = create_app({'TESTING': True})
14+
with app.test_client() as client:
15+
yield client
16+
17+
18+
@pytest.fixture
19+
def activities_json(tmpdir_factory):
20+
temporary_directory = tmpdir_factory.mktemp("tmp")
21+
json_file = temporary_directory.join("activities.json")
22+
activities = [
23+
{
24+
'id': 'c61a4a49-3364-49a3-a7f7-0c5f2d15072b',
25+
'name': 'Development',
26+
'description': 'Development',
27+
'deleted': 'b4327ba6-9f96-49ee-a9ac-3c1edf525172',
28+
'status': None,
29+
'tenant_id': 'cc925a5d-9644-4a4f-8d99-0bee49aadd05',
30+
},
31+
{
32+
'id': '94ec92e2-a500-4700-a9f6-e41eb7b5507c',
33+
'name': 'Management',
34+
'description': None,
35+
'deleted': '7cf6efe5-a221-4fe4-b94f-8945127a489a',
36+
'status': None,
37+
'tenant_id': 'cc925a5d-9644-4a4f-8d99-0bee49aadd05',
38+
},
39+
{
40+
'id': 'd45c770a-b1a0-4bd8-a713-22c01a23e41b',
41+
'name': 'Operations',
42+
'description': 'Operation activities performed.',
43+
'deleted': '7cf6efe5-a221-4fe4-b94f-8945127a489a',
44+
'status': 'active',
45+
'tenant_id': 'cc925a5d-9644-4a4f-8d99-0bee49aadd05',
46+
},
47+
]
48+
49+
with open(json_file, 'w') as outfile:
50+
json.dump(activities, outfile)
51+
52+
with open(json_file) as outfile:
53+
activities_json = json.load(outfile)
54+
55+
yield activities_json
56+
shutil.rmtree(temporary_directory)
57+
58+
59+
def test_test__activity_endpoint__returns_all_activities(
60+
client: FlaskClient, activities_json: typing.List[dict]
61+
):
62+
response = client.get("/activities/")
63+
json_data = json.loads(response.data)
64+
65+
assert response.status_code == HTTPStatus.OK
66+
assert json_data == activities_json
67+
68+
69+
def test__activity_endpoint__returns_an_activity__when_activity_matches_its_id(
70+
client: FlaskClient, activities_json: typing.List[dict]
71+
):
72+
response = client.get("/activities/%s" % activities_json[0]['id'])
73+
json_data = json.loads(response.data)
74+
75+
assert response.status_code == HTTPStatus.OK
76+
assert json_data == activities_json[0]
77+
78+
79+
def test__activity_endpoint__returns_a_not_found_status__when_no_activity_matches_its_id(
80+
client: FlaskClient,
81+
):
82+
response = client.get("/activities/%s" % Faker().uuid4())
83+
json_data = json.loads(response.data)
84+
85+
assert response.status_code == HTTPStatus.NOT_FOUND
86+
assert json_data['message'] == 'Activity not found'
File renamed without changes.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from V2.source.entry_points.flask_api.activities_endpoints import (
2+
Activities,
3+
Activity,
4+
)
5+
from V2.source import use_cases
6+
from V2.source.dtos.activity import Activity as ActivityDTO
7+
from pytest_mock import MockFixture
8+
from faker import Faker
9+
from werkzeug.exceptions import NotFound
10+
11+
fake = Faker()
12+
13+
valid_id = fake.uuid4()
14+
15+
fake_activity = {
16+
"name": fake.company(),
17+
"description": fake.paragraph(),
18+
"tenant_id": fake.uuid4(),
19+
"id": valid_id,
20+
"deleted": fake.date(),
21+
"status": fake.boolean(),
22+
}
23+
fake_activity_dto = ActivityDTO(**fake_activity)
24+
25+
26+
def test__activities_class__uses_the_get_activities_use_case__to_retrieve_activities(
27+
mocker: MockFixture,
28+
):
29+
mocker.patch.object(
30+
use_cases.GetActivitiesUseCase,
31+
'get_activities',
32+
return_value=[],
33+
)
34+
35+
activities_class_endpoint = Activities()
36+
activities = activities_class_endpoint.get()
37+
38+
assert use_cases.GetActivitiesUseCase.get_activities.called
39+
assert [] == activities
40+
41+
42+
def test__activity_class__uses_the_get_activity_by_id_use_case__to_retrieve__an_activity(
43+
mocker: MockFixture,
44+
):
45+
mocker.patch.object(
46+
use_cases.GetActivityUseCase,
47+
'get_activity_by_id',
48+
return_value=fake_activity_dto,
49+
)
50+
51+
activity_class_endpoint = Activity()
52+
activity = activity_class_endpoint.get(valid_id)
53+
54+
assert use_cases.GetActivityUseCase.get_activity_by_id.called
55+
assert fake_activity == activity
File renamed without changes.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from V2.source.services.activity_service import ActivityService
2+
from V2.source import use_cases
3+
from pytest_mock import MockFixture
4+
from faker import Faker
5+
6+
fake = Faker()
7+
8+
9+
def test__get_list_activities_function__uses_the_activities_service__to_retrieve_activities(
10+
mocker: MockFixture,
11+
):
12+
expected_activities = mocker.Mock()
13+
activity_service = mocker.Mock(
14+
get_all=mocker.Mock(return_value=expected_activities)
15+
)
16+
17+
activities_use_case = use_cases.GetActivitiesUseCase(activity_service)
18+
actual_activities = activities_use_case.get_activities()
19+
20+
assert activity_service.get_all.called
21+
assert expected_activities == actual_activities
22+
23+
24+
def test__get_activity_by_id_function__uses_the_activities_service__to_retrieve_activity(
25+
mocker: MockFixture,
26+
):
27+
expected_activity = mocker.Mock()
28+
activity_service = mocker.Mock(
29+
get_by_id=mocker.Mock(return_value=expected_activity)
30+
)
31+
32+
activity_use_case = use_cases.GetActivityUseCase(activity_service)
33+
actual_activity = activity_use_case.get_activity_by_id(fake.uuid4())
34+
35+
assert activity_service.get_by_id.called
36+
assert expected_activity == actual_activity

0 commit comments

Comments
 (0)