Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ node_modules
# Serverless directories
.serverless/

# Azure Functions json config
host.json
local.settings.json

# Files generated for development
.env
timetracker-api-postman-collection.json
Expand Down
12 changes: 11 additions & 1 deletion V2/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,14 @@ functions:
methods:
- DELETE
route: activities/{id}
authLevel: anonymous
authLevel: anonymous

update_activity:
handler: time_entries/interface.update_activity
events:
- http: true
x-azure-settings:
methods:
- PUT
route: activities/{id}
authLevel: anonymous
49 changes: 36 additions & 13 deletions V2/tests/api/azure/activity_azure_endpoints_test.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
from time_entries._application._activities import (
_get_activities,
_delete_activity,
)
from time_entries._application import _activities as activities
from faker import Faker

import azure.functions as func
import json
import typing


ACTIVITY_URL = '/api/activities/'


def test__activity_azure_endpoint__returns_all_activities(
create_temp_activities,
):
activities_json, tmp_directory = create_temp_activities
_get_activities.JSON_PATH = tmp_directory
req = func.HttpRequest(method='GET', body=None, url='/api/activities')
activities._get_activities.JSON_PATH = tmp_directory
req = func.HttpRequest(method='GET', body=None, url=ACTIVITY_URL)

response = _get_activities.get_activities(req)
response = activities.get_activities(req)
activities_json_data = response.get_body().decode("utf-8")

assert response.status_code == 200
Expand All @@ -25,15 +27,15 @@ def test__activity_azure_endpoint__returns_an_activity__when_activity_matches_it
create_temp_activities,
):
activities_json, tmp_directory = create_temp_activities
_get_activities.JSON_PATH = tmp_directory
activities._get_activities.JSON_PATH = tmp_directory
req = func.HttpRequest(
method='GET',
body=None,
url='/api/activities/',
url=ACTIVITY_URL,
route_params={"id": activities_json[0]['id']},
)

response = _get_activities.get_activities(req)
response = activities.get_activities(req)
activitiy_json_data = response.get_body().decode("utf-8")

assert response.status_code == 200
Expand All @@ -44,16 +46,37 @@ def test__activity_azure_endpoint__returns_an_activity_with_inactive_status__whe
create_temp_activities,
):
activities_json, tmp_directory = create_temp_activities
_delete_activity.JSON_PATH = tmp_directory
activities._delete_activity.JSON_PATH = tmp_directory
req = func.HttpRequest(
method='DELETE',
body=None,
url='/api/activities/',
url=ACTIVITY_URL,
route_params={"id": activities_json[0]['id']},
)

response = _delete_activity.delete_activity(req)
response = activities.delete_activity(req)
activity_json_data = json.loads(response.get_body().decode("utf-8"))

assert response.status_code == 200
assert activity_json_data['status'] == 'inactive'


def test__update_activity_azure_endpoint__returns_an_activity__when_found_an_activity_to_update(
create_temp_activities,
):
activities_json, tmp_directory = create_temp_activities
activities._update_activity.JSON_PATH = tmp_directory
activity_data = {"description": Faker().sentence()}
req = func.HttpRequest(
method='PUT',
body=json.dumps(activity_data).encode("utf-8"),
url=ACTIVITY_URL,
route_params={"id": activities_json[0]['id']},
)

response = activities.update_activity(req)
activitiy_json_data = response.get_body().decode("utf-8")
new_activity = {**activities_json[0], **activity_data}

assert response.status_code == 200
assert activitiy_json_data == json.dumps(new_activity)
65 changes: 40 additions & 25 deletions V2/tests/integration/daos/activities_json_dao_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@
import typing


fake_activities = [
{
'id': Faker().uuid4(),
'name': Faker().user_name(),
'description': Faker().sentence(),
'deleted': Faker().uuid4(),
'status': 'active',
'tenant_id': Faker().uuid4(),
}
]


@pytest.fixture(name='create_fake_activities')
def _create_fake_activities(mocker) -> typing.List[Activity]:
def _creator(activities):
Expand All @@ -20,18 +32,7 @@ def test_get_by_id__returns_an_activity_dto__when_found_one_activity_that_matche
create_fake_activities,
):
activities_json_dao = ActivitiesJsonDao(Faker().file_path())
activities = create_fake_activities(
[
{
"name": "test_name",
"description": "test_description",
"tenant_id": "test_tenant_id",
"id": "test_id",
"deleted": "test_deleted",
"status": "test_status",
}
]
)
activities = create_fake_activities(fake_activities)
activity_dto = activities.pop()

result = activities_json_dao.get_by_id(activity_dto.id)
Expand All @@ -55,19 +56,7 @@ def test__get_all__returns_a_list_of_activity_dto_objects__when_one_or_more_acti
):
activities_json_dao = ActivitiesJsonDao(Faker().file_path())
number_of_activities = 3
activities = create_fake_activities(
[
{
"name": "test_name",
"description": "test_description",
"tenant_id": "test_tenant_id",
"id": "test_id",
"deleted": "test_deleted",
"status": "test_status",
}
]
* number_of_activities
)
activities = create_fake_activities(fake_activities * number_of_activities)

result = activities_json_dao.get_all()

Expand Down Expand Up @@ -117,3 +106,29 @@ def test_delete__returns_none__when_no_activity_matching_its_id_is_found(
result = activities_json_dao.delete(Faker().uuid4())

assert result is None


def test_update__returns_an_activity_dto__when_found_one_activity_to_update(
create_fake_activities,
):
activities_json_dao = ActivitiesJsonDao(Faker().file_path())
activities = create_fake_activities(fake_activities)
activity_dto = activities.pop()
activity_data = {"description": Faker().sentence()}

result = activities_json_dao.update(activity_dto.id, activity_data)
new_activity = {**activity_dto.__dict__, **activity_data}

assert result == Activity(**new_activity)


def test_update__returns_none__when_doesnt_found_one_activity_to_update(
create_fake_activities,
):
activities_json_dao = ActivitiesJsonDao(Faker().file_path())
create_fake_activities([])
activity_data = {"description": Faker().sentence()}

result = activities_json_dao.update('', activity_data)

assert result == None
17 changes: 17 additions & 0 deletions V2/tests/unit/services/activity_service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,20 @@ def test__delete_activity__uses_the_activity_dao__to_change_activity_status(

assert activity_dao.delete.called
assert expected_activity == deleted_activity


def test__update_activity__uses_the_activity_dao__to_update_one_activity(
mocker,
):
expected_activity = mocker.Mock()
activity_dao = mocker.Mock(
update=mocker.Mock(return_value=expected_activity)
)
activity_service = ActivityService(activity_dao)

updated_activity = activity_service.update(
Faker().uuid4(), Faker().pydict()
)

assert activity_dao.update.called
assert expected_activity == updated_activity
17 changes: 17 additions & 0 deletions V2/tests/unit/use_cases/activities_use_case_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,20 @@ def test__delete_activity_function__uses_the_activity_service__to_change_activit

assert activity_service.delete.called
assert expected_activity == deleted_activity


def test__update_activity_function__uses_the_activities_service__to_update_an_activity(
mocker: MockFixture,
):
expected_activity = mocker.Mock()
activity_service = mocker.Mock(
update=mocker.Mock(return_value=expected_activity)
)

activity_use_case = _use_cases.UpdateActivityUseCase(activity_service)
updated_activity = activity_use_case.update_activity(
fake.uuid4(), fake.pydict()
)

assert activity_service.update.called
assert expected_activity == updated_activity
1 change: 1 addition & 0 deletions V2/time_entries/_application/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from ._activities import get_activities
from ._activities import delete_activity
from ._activities import update_activity
1 change: 1 addition & 0 deletions V2/time_entries/_application/_activities/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from ._get_activities import get_activities
from ._delete_activity import delete_activity
from ._update_activity import update_activity
44 changes: 44 additions & 0 deletions V2/time_entries/_application/_activities/_update_activity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from time_entries._infrastructure import ActivitiesJsonDao
from time_entries._domain import ActivityService, Activity, _use_cases

import azure.functions as func
import dataclasses
import json
import logging

JSON_PATH = (
'time_entries/_infrastructure/_data_persistence/activities_data.json'
)


def update_activity(req: func.HttpRequest) -> func.HttpResponse:
logging.info(
'Python HTTP trigger function processed a request to update an activity.'
)
activity_id = req.route_params.get('id')
activity_data = req.get_json() if req.get_body() else {}
activity_keys = [field.name for field in dataclasses.fields(Activity)]

if all(key in activity_keys for key in activity_data.keys()):
response = _update(activity_id, activity_data)
status_code = 200
else:
response = b'Incorrect activity body'
status_code = 400

return func.HttpResponse(
body=response, status_code=status_code, mimetype="application/json"
)


def _update(activity_id: str, activity_data: dict) -> str:
activity_use_case = _use_cases.UpdateActivityUseCase(
_create_activity_service(JSON_PATH)
)
activity = activity_use_case.update_activity(activity_id, activity_data)
return json.dumps(activity.__dict__) if activity else b'Not Found'


def _create_activity_service(path: str):
activity_json = ActivitiesJsonDao(path)
return ActivityService(activity_json)
6 changes: 5 additions & 1 deletion V2/time_entries/_domain/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from ._entities import Activity
from ._persistence_contracts import ActivitiesDao
from ._services import ActivityService
from ._use_cases import GetActivitiesUseCase, GetActivityUseCase
from ._use_cases import (
GetActivitiesUseCase,
GetActivityUseCase,
UpdateActivityUseCase,
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ def get_all(self) -> typing.List[Activity]:
@abc.abstractmethod
def delete(self, id: str) -> Activity:
pass

@abc.abstractmethod
def update(self, id: str, new_activity: dict) -> Activity:
pass
3 changes: 3 additions & 0 deletions V2/time_entries/_domain/_services/_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 update(self, activity_id: str, new_activity: dict) -> Activity:
return self.activities_dao.update(activity_id, new_activity)
1 change: 1 addition & 0 deletions V2/time_entries/_domain/_use_cases/__init__.py
Original file line number Diff line number Diff line change
@@ -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 ._update_activity_use_case import UpdateActivityUseCase
11 changes: 11 additions & 0 deletions V2/time_entries/_domain/_use_cases/_update_activity_use_case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from time_entries._domain import ActivityService, Activity


class UpdateActivityUseCase:
def __init__(self, activity_service: ActivityService):
self.activity_service = activity_service

def update_activity(
self, activity_id: str, new_activity: dict
) -> Activity:
return self.activity_service.update(activity_id, new_activity)
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,32 @@ def delete(self, activity_id: str) -> Activity:
else:
return None

def update(self, activity_id: str, new_activity: dict) -> Activity:
activity = self.get_by_id(activity_id)
if not activity:
return None

new_activity = {**activity.__dict__, **new_activity}

activities_updated = list(
map(
lambda activity: activity
if activity.get('id') != activity_id
else new_activity,
self.__get_activities_from_file(),
)
)

try:
file = open(self.json_data_file_path, 'w')
json.dump(activities_updated, file)
file.close()

return self.__create_activity_dto(new_activity)

except FileNotFoundError:
return None

def __get_activities_from_file(self) -> typing.List[dict]:
try:
file = open(self.json_data_file_path)
Expand Down
1 change: 1 addition & 0 deletions V2/time_entries/interface.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from ._application import get_activities
from ._application import delete_activity
from ._application import update_activity