From fb222c1a5f13b8313a257159f3115cf9c0fae750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean=20Carlos=20Alarc=C3=B3n?= <56373098+jcalarcon98@users.noreply.github.com> Date: Fri, 9 Jul 2021 15:44:45 -0500 Subject: [PATCH 1/3] fix: TT-287 Fix users email doesn't show in reports page (#305) fix: TT-287 Add test for AzureConnection.users function --- tests/utils/azure_users_test.py | 43 ++++++++++++++++++++++++++++++--- utils/azure_users.py | 32 ++++++++++++++++++++---- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/tests/utils/azure_users_test.py b/tests/utils/azure_users_test.py index e7ac2db6..0efe4144 100644 --- a/tests/utils/azure_users_test.py +++ b/tests/utils/azure_users_test.py @@ -1,4 +1,7 @@ +import copy from unittest.mock import Mock, patch +from requests import Response + from utils.azure_users import AzureConnection, ROLE_FIELD_VALUES, AzureUser from pytest import mark @@ -14,7 +17,10 @@ ], ) def test_azure_connection_is_test_user( - get_mock, field_name, field_value, is_test_user_expected_value, + get_mock, + field_name, + field_value, + is_test_user_expected_value, ): response_mock = Mock() response_mock.status_code = 200 @@ -33,7 +39,12 @@ def test_azure_connection_get_test_user_ids(get_mock): response_mock = Mock() response_mock.status_code = 200 response_mock.json = Mock( - return_value={'value': [{'objectId': 'ID1'}, {'objectId': 'ID2'},]} + return_value={ + 'value': [ + {'objectId': 'ID1'}, + {'objectId': 'ID2'}, + ] + } ) get_mock.return_value = response_mock @@ -120,7 +131,10 @@ def test_get_groups_and_users(get_mock): {'objectId': 'user-id1'}, ], }, - {'displayName': 'test-group-3', 'members': [],}, + { + 'displayName': 'test-group-3', + 'members': [], + }, ] } response_mock.json = Mock(return_value=return_value) @@ -228,3 +242,26 @@ def test_remove_user_from_group( get_group_id_by_group_name_mock.assert_called_once() get_user_mock.assert_called_once() assert expected_value == test_user + + +@patch('utils.azure_users.AzureConnection.get_groups_and_users') +@patch('requests.get') +def test_users_functions_should_returns_all_users( + get_mock, get_groups_and_users_mock +): + first_response = Response() + first_response.status_code = 200 + first_response._content = ( + b'{"odata.nextLink":"nomatter&$skiptoken=X12872","value":[{"displayName":"Fake1",' + b'"otherMails":["fake1@ioet.com"],"objectId":"1"}]} ' + ) + + second_response = copy.copy(first_response) + second_response._content = b'{"value":[{"displayName":"Fake2","otherMails":["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 diff --git a/utils/azure_users.py b/utils/azure_users.py index 4a1c2e4b..376f8937 100644 --- a/utils/azure_users.py +++ b/utils/azure_users.py @@ -100,11 +100,32 @@ def users(self) -> List[AzureUser]: endpoint=self.config.ENDPOINT, role_fields_params=role_fields_params, ) - response = requests.get(endpoint, auth=BearerAuth(self.access_token)) - assert 200 == response.status_code - assert 'value' in response.json() - return [self.to_azure_user(item) for item in response.json()['value']] + exists_users = True + users = [] + skip_token_attribute = '&$skiptoken=' + + while exists_users: + response = requests.get( + endpoint, auth=BearerAuth(self.access_token) + ) + json_response = response.json() + assert 200 == response.status_code + assert 'value' in json_response + users = users + json_response['value'] + remaining_users_link = json_response.get('odata.nextLink', None) + exists_users = ( + False + if remaining_users_link is None + else skip_token_attribute in remaining_users_link + ) + if exists_users: + request_token = remaining_users_link.split( + skip_token_attribute + )[1] + endpoint = endpoint + skip_token_attribute + request_token + + return [self.to_azure_user(user) for user in users] def to_azure_user(self, item) -> AzureUser: there_is_email = len(item['otherMails']) > 0 @@ -142,7 +163,8 @@ def update_role(self, user_id, role_id, is_grant): def add_user_to_group(self, user_id, group_name): group_id = self.get_group_id_by_group_name(group_name) endpoint = "{endpoint}/groups/{group_id}/$links/members?api-version=1.6".format( - endpoint=self.config.ENDPOINT, group_id=group_id, + endpoint=self.config.ENDPOINT, + group_id=group_id, ) data = {'url': f'{self.config.ENDPOINT}/directoryObjects/{user_id}'} response = requests.post( From 5f95b5bd3bb1aa6632641736a115949377383be0 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Fri, 9 Jul 2021 20:53:12 +0000 Subject: [PATCH 2/3] 0.36.2 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 210a30a1..81c121e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ +## v0.36.2 (2021-07-09) +### Fix +* TT-287 Fix users email doesn't show in reports page ([#305](https://github.com/ioet/time-tracker-backend/issues/305)) ([`fb222c1`](https://github.com/ioet/time-tracker-backend/commit/fb222c1a5f13b8313a257159f3115cf9c0fae750)) + ## v0.36.1 (2021-07-08) ### Fix * TT-270 Fix handle exceptions in create update entries (#301) ([#306](https://github.com/ioet/time-tracker-backend/issues/306)) ([`7b74f46`](https://github.com/ioet/time-tracker-backend/commit/7b74f460586bc5f756067a01491a7670b6751529)) diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index eb426c54..349a8f7b 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.36.1' +__version__ = '0.36.2' From 954775423a8454cbfa0f701c2d769606a6bd787a Mon Sep 17 00:00:00 2001 From: PieritoAlva95 Date: Thu, 8 Jul 2021 11:39:50 -0500 Subject: [PATCH 3/3] fix: TT-274 fix error on archive a customer removes the project --- .gitignore | 4 +- .../projects/projects_model_test.py | 61 +++++++++++++++ .../project_types/project_types_model.py | 27 ++++--- time_tracker_api/projects/projects_model.py | 14 ++-- .../projects/projects_namespace.py | 74 ++++++++++--------- 5 files changed, 130 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index 9699da54..c4f6932f 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,6 @@ migration_status.csv env.* # SSL certificate for cosmos emulator -emulatorcert.crt \ No newline at end of file +emulatorcert.crt + +seed_database.json \ No newline at end of file diff --git a/tests/time_tracker_api/projects/projects_model_test.py b/tests/time_tracker_api/projects/projects_model_test.py index 8579ab30..c6b63c40 100644 --- a/tests/time_tracker_api/projects/projects_model_test.py +++ b/tests/time_tracker_api/projects/projects_model_test.py @@ -5,11 +5,18 @@ CustomerCosmosDBModel, CustomerCosmosDBDao, ) +from time_tracker_api.project_types.project_types_model import ( + ProjectTypeCosmosDBModel, + ProjectTypeCosmosDBDao, +) from time_tracker_api.projects.projects_model import ( ProjectCosmosDBRepository, ProjectCosmosDBModel, create_dao, ) +from faker import Faker + +fake = Faker() @patch( @@ -77,3 +84,57 @@ def test_get_project_with_their_customer( assert isinstance(project, ProjectCosmosDBModel) assert project.__dict__['customer_name'] == customer_data['name'] + + +def test_get_all_projects_with_customers( + mocker, +): + customer_id = fake.uuid4() + project_type_id = fake.uuid4() + + customer_data = { + 'id': customer_id, + 'name': fake.company(), + 'description': fake.paragraph(), + 'tenant_id': fake.uuid4(), + } + + project_data = { + 'customer_id': customer_id, + 'id': fake.uuid4(), + 'name': fake.company(), + 'description': fake.paragraph(), + 'project_type_id': project_type_id, + 'tenant_id': fake.uuid4(), + } + + project_type_dao = { + 'id': project_type_id, + 'name': fake.name(), + 'description': fake.paragraph(), + 'tenant_id': fake.uuid4(), + } + + expected_customer = CustomerCosmosDBModel(customer_data) + expected_project = ProjectCosmosDBModel(project_data) + expected_project_type = ProjectTypeCosmosDBModel(project_type_dao) + + customer_dao_get_all_mock = mocker.patch.object( + CustomerCosmosDBDao, 'get_all' + ) + customer_dao_get_all_mock.return_value = [expected_customer] + + projects_repository_find_all_mock = mocker.patch.object( + ProjectCosmosDBRepository, 'find_all' + ) + projects_repository_find_all_mock.return_value = [expected_project] + + project_type_dao_get_all_mock = mocker.patch.object( + ProjectTypeCosmosDBDao, 'get_all' + ) + project_type_dao_get_all_mock.return_value = [expected_project_type] + projects = create_dao().get_all() + + assert isinstance(projects[0], ProjectCosmosDBModel) + assert projects[0].__dict__['customer_name'] == customer_data['name'] + assert len(projects) == 1 diff --git a/time_tracker_api/project_types/project_types_model.py b/time_tracker_api/project_types/project_types_model.py index 47f1eb13..9024e73e 100644 --- a/time_tracker_api/project_types/project_types_model.py +++ b/time_tracker_api/project_types/project_types_model.py @@ -2,7 +2,11 @@ from azure.cosmos import PartitionKey -from commons.data_access_layer.cosmos_db import CosmosDBModel, CosmosDBDao, CosmosDBRepository +from commons.data_access_layer.cosmos_db import ( + CosmosDBModel, + CosmosDBDao, + CosmosDBRepository, +) from time_tracker_api.database import CRUDDao, APICosmosDBDao @@ -17,7 +21,7 @@ class ProjectTypeDao(CRUDDao): 'uniqueKeys': [ {'paths': ['/name', '/customer_id', '/deleted']}, ] - } + }, } @@ -32,7 +36,9 @@ class ProjectTypeCosmosDBModel(CosmosDBModel): tenant_id: str def __init__(self, data): - super(ProjectTypeCosmosDBModel, self).__init__(data) # pragma: no cover + super(ProjectTypeCosmosDBModel, self).__init__( + data + ) # pragma: no cover def __repr__(self): return '' % self.name # pragma: no cover @@ -41,12 +47,13 @@ def __str___(self): return "the project type \"%s\"" % self.name # pragma: no cover -def create_dao() -> ProjectTypeDao: - repository = CosmosDBRepository.from_definition(container_definition, - mapper=ProjectTypeCosmosDBModel) +class ProjectTypeCosmosDBDao(APICosmosDBDao, ProjectTypeDao): + def __init__(self, repository): + CosmosDBDao.__init__(self, repository) - class ProjectTypeCosmosDBDao(APICosmosDBDao, ProjectTypeDao): - def __init__(self): - CosmosDBDao.__init__(self, repository) - return ProjectTypeCosmosDBDao() +def create_dao() -> ProjectTypeDao: + repository = CosmosDBRepository.from_definition( + container_definition, mapper=ProjectTypeCosmosDBModel + ) + return ProjectTypeCosmosDBDao(repository) diff --git a/time_tracker_api/projects/projects_model.py b/time_tracker_api/projects/projects_model.py index 805d6130..0d767251 100644 --- a/time_tracker_api/projects/projects_model.py +++ b/time_tracker_api/projects/projects_model.py @@ -134,15 +134,17 @@ def get_all( """ event_ctx = self.create_event_context("read-many") customer_dao = customers_create_dao() + customer_status = kwargs.get('customer_status', None) + customer_conditions = ( + {'status': customer_status} if customer_status else None + ) + customers = customer_dao.get_all( - max_count=kwargs.get('max_count', None) + conditions=customer_conditions, + max_count=kwargs.get('max_count', None), ) - customers_id = [ - customer.id - for customer in customers - if customer.status == 'active' - ] + customers_id = [customer.id for customer in customers] conditions = conditions if conditions else {} diff --git a/time_tracker_api/projects/projects_namespace.py b/time_tracker_api/projects/projects_namespace.py index 82dbaf2b..7030f63e 100644 --- a/time_tracker_api/projects/projects_namespace.py +++ b/time_tracker_api/projects/projects_namespace.py @@ -70,39 +70,45 @@ }, ) -project_type_nested_field = ns.model('ProjectType', { - 'name': fields.String( - title='Name', - required=True, - max_length=50, - description='Name of the project type', - example=faker.random_element(["Customer", "Training", "Internal"]), - ), - 'description': NullableString( - title='Description', - required=False, - max_length=250, - description='Comments about the project type', - example=faker.paragraph(), - ) -}) +project_type_nested_field = ns.model( + 'ProjectType', + { + 'name': fields.String( + title='Name', + required=True, + max_length=50, + description='Name of the project type', + example=faker.random_element(["Customer", "Training", "Internal"]), + ), + 'description': NullableString( + title='Description', + required=False, + max_length=250, + description='Comments about the project type', + example=faker.paragraph(), + ), + }, +) -customer_nested_field = ns.model('Customer', { - 'name': fields.String( - title='Name', - required=True, - max_length=50, - description='Name of the customer', - example=faker.company(), - ), - 'description': NullableString( - title='Description', - required=False, - max_length=250, - description='Description about the customer', - example=faker.paragraph(), - ) -}) +customer_nested_field = ns.model( + 'Customer', + { + 'name': fields.String( + title='Name', + required=True, + max_length=50, + description='Name of the customer', + example=faker.company(), + ), + 'description': NullableString( + title='Description', + required=False, + max_length=250, + description='Description about the customer', + example=faker.paragraph(), + ), + }, +) project_response_fields = { # TODO: Remove this DEAD CODE @@ -135,7 +141,9 @@ class Projects(Resource): def get(self): """List all projects""" conditions = attributes_filter.parse_args() - return project_dao.get_all(conditions=conditions) + return project_dao.get_all( + conditions=conditions, customer_status='active' + ) @ns.doc('create_project') @ns.response(HTTPStatus.CONFLICT, 'This project already exists')