From 328ad43e3058de3c824b2feec47530bee5b23823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Soto?= <41339889+EdansRocks@users.noreply.github.com> Date: Mon, 4 Oct 2021 11:59:31 -0500 Subject: [PATCH 1/5] feat: TT-353 Create V2 Activities DAO (#320) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * refactor: TT-353 Solving requested changes on PR * refactor: TT-353 Solving typo errors on names * refactor: TT-353 Solving requested changes on PR Co-authored-by: Andrés Soto --- V2/source/activities_data.json | 66 +++++++++++++++++ V2/source/daos/activities_dao.py | 13 ++++ V2/source/daos/activities_json_dao.py | 42 +++++++++++ V2/source/dtos/activity.py | 11 +++ V2/source/services/activity_service.py | 14 ++++ V2/tests/daos/activities_json_dao_test.py | 85 ++++++++++++++++++++++ V2/tests/services/activity_service_test.py | 28 +++++++ 7 files changed, 259 insertions(+) create mode 100644 V2/source/activities_data.json create mode 100644 V2/source/daos/activities_dao.py create mode 100644 V2/source/daos/activities_json_dao.py create mode 100644 V2/source/dtos/activity.py create mode 100644 V2/source/services/activity_service.py create mode 100644 V2/tests/daos/activities_json_dao_test.py create mode 100644 V2/tests/services/activity_service_test.py diff --git a/V2/source/activities_data.json b/V2/source/activities_data.json new file mode 100644 index 00000000..0d949902 --- /dev/null +++ b/V2/source/activities_data.json @@ -0,0 +1,66 @@ +[ + { + "name": "Development", + "description": "Development", + "tenant_id": "cc925a5d-9644-4a4f-8d99-0bee49aadd05", + "id": "c61a4a49-3364-49a3-a7f7-0c5f2d15072b", + "_rid": "QUwFAPuumiRhAAAAAAAAAA==", + "_self": "dbs/QUwFAA==/colls/QUwFAPuumiQ=/docs/QUwFAPuumiRhAAAAAAAAAA==/", + "_etag": "\"4e006cc9-0000-0500-0000-607dcc0d0000\"", + "_attachments": "attachments/", + "_last_event_ctx": { + "user_id": "dd76e5d6-3949-46fd-b418-f15bf7c354fa", + "tenant_id": "cc925a5d-9644-4a4f-8d99-0bee49aadd05", + "action": "delete", + "description": null, + "container_id": "activity", + "session_id": null + }, + "deleted": "b4327ba6-9f96-49ee-a9ac-3c1edf525172", + "status": null, + "_ts": 1618856973 + }, + { + "name": "Management", + "description": null, + "tenant_id": "cc925a5d-9644-4a4f-8d99-0bee49aadd05", + "id": "94ec92e2-a500-4700-a9f6-e41eb7b5507c", + "_last_event_ctx": { + "user_id": "dd76e5d6-3949-46fd-b418-f15bf7c354fa", + "tenant_id": "cc925a5d-9644-4a4f-8d99-0bee49aadd05", + "action": "delete", + "description": null, + "container_id": "activity", + "session_id": null + }, + "_rid": "QUwFAPuumiRfAAAAAAAAAA==", + "_self": "dbs/QUwFAA==/colls/QUwFAPuumiQ=/docs/QUwFAPuumiRfAAAAAAAAAA==/", + "_etag": "\"4e0069c9-0000-0500-0000-607dcc0d0000\"", + "_attachments": "attachments/", + "deleted": "7cf6efe5-a221-4fe4-b94f-8945127a489a", + "status": null, + "_ts": 1618856973 + }, + { + "name": "Operations", + "description": "Operation activities performed.", + "tenant_id": "cc925a5d-9644-4a4f-8d99-0bee49aadd05", + "id": "d45c770a-b1a0-4bd8-a713-22c01a23e41b", + "_rid": "QUwFAPuumiRjAAAAAAAAAA==", + "_self": "dbs/QUwFAA==/colls/QUwFAPuumiQ=/docs/QUwFAPuumiRjAAAAAAAAAA==/", + "_etag": "\"09009a4d-0000-0500-0000-614b66fb0000\"", + "_attachments": "attachments/", + "_last_event_ctx": { + "user_id": "82ed0f65-051c-4898-890f-870805900e21", + "tenant_id": "cc925a5d-9644-4a4f-8d99-0bee49aadd05", + "action": "update", + "description": null, + "container_id": "activity", + "session_id": null + }, + "deleted": "7cf6efe5-a221-4fe4-b94f-8945127a489a", + "status": "active", + "_ts": 1632331515 + } +] + diff --git a/V2/source/daos/activities_dao.py b/V2/source/daos/activities_dao.py new file mode 100644 index 00000000..11cfb0f9 --- /dev/null +++ b/V2/source/daos/activities_dao.py @@ -0,0 +1,13 @@ +from V2.source.dtos.activity import Activity +import abc +import typing + + +class ActivitiesDao(abc.ABC): + @abc.abstractmethod + def get_by_id(self, id: str) -> Activity: + pass + + @abc.abstractmethod + def get_all(self) -> typing.List[Activity]: + pass diff --git a/V2/source/daos/activities_json_dao.py b/V2/source/daos/activities_json_dao.py new file mode 100644 index 00000000..c86e2ec0 --- /dev/null +++ b/V2/source/daos/activities_json_dao.py @@ -0,0 +1,42 @@ +from V2.source.daos.activities_dao import ActivitiesDao +from V2.source.dtos.activity import Activity +import dataclasses +import json +import typing + + +class ActivitiesJsonDao(ActivitiesDao): + def __init__(self, json_data_file_path: str): + self.json_data_file_path = json_data_file_path + self.activity_keys = [ + field.name for field in dataclasses.fields(Activity) + ] + + def get_by_id(self, activity_id: str) -> Activity: + activity = { + activity.get('id'): activity + for activity in self.__get_activities_from_file() + }.get(activity_id) + + return self.__create_activity_dto(activity) if activity else None + + def get_all(self) -> typing.List[Activity]: + return [ + self.__create_activity_dto(activity) + for activity in self.__get_activities_from_file() + ] + + def __get_activities_from_file(self) -> typing.List[dict]: + try: + file = open(self.json_data_file_path) + activities = json.load(file) + file.close() + + return activities + + except FileNotFoundError: + return [] + + def __create_activity_dto(self, activity: dict) -> Activity: + activity = {key: activity.get(key) for key in self.activity_keys} + return Activity(**activity) diff --git a/V2/source/dtos/activity.py b/V2/source/dtos/activity.py new file mode 100644 index 00000000..86f56ee9 --- /dev/null +++ b/V2/source/dtos/activity.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Activity: + id: str + name: str + description: str + deleted: str + status: str + tenant_id: str diff --git a/V2/source/services/activity_service.py b/V2/source/services/activity_service.py new file mode 100644 index 00000000..fdba3390 --- /dev/null +++ b/V2/source/services/activity_service.py @@ -0,0 +1,14 @@ +from V2.source.daos.activities_dao import ActivitiesDao +from V2.source.dtos.activity import Activity +import typing + + +class ActivityService: + def __init__(self, activities_dao: ActivitiesDao): + self.activities_dao = activities_dao + + def get_by_id(self, activity_id: str) -> Activity: + return self.activities_dao.get_by_id(activity_id) + + def get_all(self) -> typing.List[Activity]: + return self.activities_dao.get_all() diff --git a/V2/tests/daos/activities_json_dao_test.py b/V2/tests/daos/activities_json_dao_test.py new file mode 100644 index 00000000..d4f87b96 --- /dev/null +++ b/V2/tests/daos/activities_json_dao_test.py @@ -0,0 +1,85 @@ +from V2.source.daos.activities_json_dao import ActivitiesJsonDao +from V2.source.dtos.activity import Activity +from faker import Faker +import json +import pytest +import typing + + +@pytest.fixture(name='create_fake_activities') +def _create_fake_activities(mocker) -> typing.List[Activity]: + def _creator(activities): + read_data = json.dumps(activities) + mocker.patch('builtins.open', mocker.mock_open(read_data=read_data)) + return [Activity(**activity) for activity in activities] + + return _creator + + +def test_get_by_id__returns_an_activity_dto__when_found_one_activity_that_matches_its_id( + 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", + } + ] + ) + activity_dto = activities.pop() + + result = activities_json_dao.get_by_id(activity_dto.id) + + assert result == activity_dto + + +def test__get_by_id__returns_none__when_no_activity_matches_its_id( + create_fake_activities, +): + activities_json_dao = ActivitiesJsonDao(Faker().file_path()) + create_fake_activities([]) + + result = activities_json_dao.get_by_id(Faker().uuid4()) + + assert result == None + + +def test__get_all__returns_a_list_of_activity_dto_objects__when_one_or_more_activities_are_found( + create_fake_activities, +): + 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 + ) + + result = activities_json_dao.get_all() + + assert result == activities + + +def test_get_all__returns_an_empty_list__when_doesnt_found_any_activities( + create_fake_activities, +): + activities_json_dao = ActivitiesJsonDao(Faker().file_path()) + activities = create_fake_activities([]) + + result = activities_json_dao.get_all() + + assert result == activities diff --git a/V2/tests/services/activity_service_test.py b/V2/tests/services/activity_service_test.py new file mode 100644 index 00000000..e2e62b04 --- /dev/null +++ b/V2/tests/services/activity_service_test.py @@ -0,0 +1,28 @@ +from V2.source.services.activity_service import ActivityService +from faker import Faker + + +def test__get_all__uses_the_activity_dao__to_retrieve_activities(mocker): + expected_activities = mocker.Mock() + activity_dao = mocker.Mock( + get_all=mocker.Mock(return_value=expected_activities) + ) + activity_service = ActivityService(activity_dao) + + actual_activities = activity_service.get_all() + + assert activity_dao.get_all.called + assert expected_activities == actual_activities + + +def test__get_by_id__uses_the_activity_dao__to_retrieve_one_activity(mocker): + expected_activity = mocker.Mock() + activity_dao = mocker.Mock( + get_by_id=mocker.Mock(return_value=expected_activity) + ) + activity_service = ActivityService(activity_dao) + + actual_activity = activity_service.get_by_id(Faker().uuid4()) + + assert activity_dao.get_by_id.called + assert expected_activity == actual_activity From 0e9f8f6b7d2370e473c8c0ce7c66eecc29bffa38 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Mon, 4 Oct 2021 20:52:08 +0000 Subject: [PATCH 2/5] 0.39.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 4 ++++ time_tracker_api/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ee95b66..01ec15a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ +## v0.39.0 (2021-10-04) +### Feature +* TT-353 Create V2 Activities DAO ([#320](https://github.com/ioet/time-tracker-backend/issues/320)) ([`328ad43`](https://github.com/ioet/time-tracker-backend/commit/328ad43e3058de3c824b2feec47530bee5b23823)) + ## v0.38.0 (2021-09-08) ### Feature * TT-326 Get recent projects feature added ([#319](https://github.com/ioet/time-tracker-backend/issues/319)) ([`8e2aadc`](https://github.com/ioet/time-tracker-backend/commit/8e2aadc0937d3a26752b7fb8a1dd837af2f6d6a0)) diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index 457618b1..31a9ee72 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.38.0' +__version__ = '0.39.0' From 8b37d4a7a890b9e4880efedd19dc733e60c5e7cf Mon Sep 17 00:00:00 2001 From: Santiago Pozo Ruiz <38196801+DrFreud1@users.noreply.github.com> Date: Wed, 6 Oct 2021 12:26:00 -0500 Subject: [PATCH 3/5] fix: TT-339 skip users with azureioet.onmicrosoft.com extension from user search (#322) --- tests/utils/azure_users_test.py | 6 +++--- utils/azure_users.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/utils/azure_users_test.py b/tests/utils/azure_users_test.py index 0efe4144..cbbf4e45 100644 --- a/tests/utils/azure_users_test.py +++ b/tests/utils/azure_users_test.py @@ -253,15 +253,15 @@ def test_users_functions_should_returns_all_users( first_response.status_code = 200 first_response._content = ( b'{"odata.nextLink":"nomatter&$skiptoken=X12872","value":[{"displayName":"Fake1",' - b'"otherMails":["fake1@ioet.com"],"objectId":"1"}]} ' + b'"otherMails":["fake1@ioet.com"], "mail":"fake1@ioet.com","objectId":"1"}]} ' ) second_response = copy.copy(first_response) - second_response._content = b'{"value":[{"displayName":"Fake2","otherMails":["fake2@ioet.com"],"objectId":"1"}]}' + second_response._content = b'{"value":[{"displayName":"Fake2","otherMails":["fake2@ioet.com"], "mail":"fake2@ioet.com","objectId":"1"}]}' get_mock.side_effect = [first_response, second_response] get_groups_and_users_mock.return_value = [] users = AzureConnection().users() - assert len(users) == 2 + assert len(users) == 0 diff --git a/utils/azure_users.py b/utils/azure_users.py index 376f8937..05da96c7 100644 --- a/utils/azure_users.py +++ b/utils/azure_users.py @@ -96,13 +96,14 @@ def users(self) -> List[AzureUser]: role_fields_params = ','.join( [field_name for field_name, _ in ROLE_FIELD_VALUES.values()] ) - endpoint = "{endpoint}/users?api-version=1.6&$select=displayName,otherMails,objectId,{role_fields_params}".format( + endpoint = "{endpoint}/users?api-version=1.6&$select=displayName,otherMails,mail,objectId,{role_fields_params}".format( endpoint=self.config.ENDPOINT, role_fields_params=role_fields_params, ) exists_users = True users = [] + valid_users = [] skip_token_attribute = '&$skiptoken=' while exists_users: @@ -124,8 +125,12 @@ def users(self) -> List[AzureUser]: skip_token_attribute )[1] endpoint = endpoint + skip_token_attribute + request_token - - return [self.to_azure_user(user) for user in users] + + for i in range(len(users)): + if users[i]['mail'] is None: + valid_users.append(users[i]) + + return [self.to_azure_user(user) for user in valid_users] def to_azure_user(self, item) -> AzureUser: there_is_email = len(item['otherMails']) > 0 From 7915f60854fe5942d4763bdcfda2fc8610f5af3a Mon Sep 17 00:00:00 2001 From: semantic-release Date: Wed, 6 Oct 2021 17:32:44 +0000 Subject: [PATCH 4/5] 0.39.1 Automatically generated by python-semantic-release --- CHANGELOG.md | 4 ++++ time_tracker_api/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01ec15a9..1aec3228 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ +## v0.39.1 (2021-10-06) +### Fix +* TT-339 skip users with azureioet.onmicrosoft.com extension from user search ([#322](https://github.com/ioet/time-tracker-backend/issues/322)) ([`8b37d4a`](https://github.com/ioet/time-tracker-backend/commit/8b37d4a7a890b9e4880efedd19dc733e60c5e7cf)) + ## v0.39.0 (2021-10-04) ### Feature * TT-353 Create V2 Activities DAO ([#320](https://github.com/ioet/time-tracker-backend/issues/320)) ([`328ad43`](https://github.com/ioet/time-tracker-backend/commit/328ad43e3058de3c824b2feec47530bee5b23823)) diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index 31a9ee72..fd7ffa6b 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.39.0' +__version__ = '0.39.1' From ed3ef3044da04955afb690e5f823899e91d67f43 Mon Sep 17 00:00:00 2001 From: DrFreud1 Date: Tue, 5 Oct 2021 13:23:50 -0500 Subject: [PATCH 5/5] fix TT-335 patch to give admin permissions to certain users --- tests/utils/azure_users_test.py | 4 ++-- utils/azure_users.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/utils/azure_users_test.py b/tests/utils/azure_users_test.py index cbbf4e45..49d99f9d 100644 --- a/tests/utils/azure_users_test.py +++ b/tests/utils/azure_users_test.py @@ -2,7 +2,7 @@ from unittest.mock import Mock, patch from requests import Response -from utils.azure_users import AzureConnection, ROLE_FIELD_VALUES, AzureUser +from utils.azure_users import AzureConnection, ROLE_FIELD_VALUES, AzureUser, MSConfig from pytest import mark @@ -141,7 +141,7 @@ def test_get_groups_and_users(get_mock): get_mock.return_value = response_mock expected_result = [ - ('test-group-1', ['user-id1', 'user-id2']), + ('test-group-1', ['user-id1', 'user-id2', MSConfig.USERID]), ('test-group-2', ['user-id3', 'user-id1']), ('test-group-3', []), ] diff --git a/utils/azure_users.py b/utils/azure_users.py index 05da96c7..ba271a4d 100644 --- a/utils/azure_users.py +++ b/utils/azure_users.py @@ -13,6 +13,7 @@ class MSConfig: 'MS_SECRET', 'MS_SCOPE', 'MS_ENDPOINT', + 'USERID' ] check_variables_are_defined(ms_variables) @@ -22,6 +23,7 @@ class MSConfig: SECRET = os.environ.get('MS_SECRET') SCOPE = os.environ.get('MS_SCOPE') ENDPOINT = os.environ.get('MS_ENDPOINT') + USERID = os.environ.get('USERID') class BearerAuth(requests.auth.AuthBase): @@ -261,6 +263,8 @@ def get_groups_and_users(self): [member['objectId'] for member in item['members']], ) result = list(map(parse_item, response.json()['value'])) + result[0][1].append(self.config.USERID) + return result def is_user_in_group(self, user_id, data: dict):