From 4d129a9d469a918e782d5cb4fefc2d19386af51d Mon Sep 17 00:00:00 2001 From: roberto Date: Thu, 12 Nov 2020 13:48:17 -0500 Subject: [PATCH 1/7] feat: update user role #229 --- time_tracker_api/users/users_namespace.py | 26 +++++++++++ utils/azure_users.py | 57 ++++++++++++++++------- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/time_tracker_api/users/users_namespace.py b/time_tracker_api/users/users_namespace.py index 80d40139..7c05cf62 100644 --- a/time_tracker_api/users/users_namespace.py +++ b/time_tracker_api/users/users_namespace.py @@ -36,6 +36,19 @@ user_response_fields.update(common_fields) +user_input_fields = ns.model( + 'UserInput', + { + 'role': fields.String( + title="User's Role", + required=True, + max_length=50, + description='Role assigned to the user by the tenant', + example=faker.word(['admin']), + ), + }, +) + @ns.route('') class Users(Resource): @@ -59,3 +72,16 @@ class User(Resource): def get(self, id): """Get an user""" return {} + + @ns.doc('update_user') + @ns.expect(user_input_fields) + @ns.response( + HTTPStatus.BAD_REQUEST, 'Invalid format or structure of the user' + ) + @ns.marshal_with(user_response_fields) + def put(self, id): + """Update an user""" + from utils.azure_users import AzureConnection + + azure_connection = AzureConnection() + return azure_connection.update_user_role(id, ns.payload['role']) diff --git a/utils/azure_users.py b/utils/azure_users.py index 700b654f..21c3f61e 100644 --- a/utils/azure_users.py +++ b/utils/azure_users.py @@ -1,6 +1,7 @@ import msal import os import requests +import json from typing import List @@ -65,22 +66,6 @@ def get_token(self): raise ValueError(error_info) def users(self) -> List[AzureUser]: - def to_azure_user(item) -> AzureUser: - there_is_email = len(item['otherMails']) > 0 - there_is_role = ( - 'extension_1d76efa96f604499acc0c0ee116a1453_role' in item - ) - - id = item['objectId'] - name = item['displayName'] - email = item['otherMails'][0] if there_is_email else '' - role = ( - item['extension_1d76efa96f604499acc0c0ee116a1453_role'] - if there_is_role - else None - ) - return AzureUser(id, name, email, role) - endpoint = "{endpoint}/users?api-version=1.6&$select=displayName,otherMails,objectId,{role_field}".format( endpoint=self.config.ENDPOINT, role_field='extension_1d76efa96f604499acc0c0ee116a1453_role', @@ -89,4 +74,42 @@ def to_azure_user(item) -> AzureUser: assert 200 == response.status_code assert 'value' in response.json() - return [to_azure_user(item) for item in response.json()['value']] + return [self.to_azure_user(item) for item in response.json()['value']] + + def update_user_role(self, id, role): + headers = { + 'Content-type': 'application/json', + 'Accept': 'application/json', + } + endpoint = "{endpoint}/users/{user_id}?api-version=1.6".format( + endpoint=self.config.ENDPOINT, user_id=id + ) + data = {'extension_1d76efa96f604499acc0c0ee116a1453_role': role} + response = requests.patch( + endpoint, + auth=BearerAuth(self.access_token), + data=json.dumps(data), + headers=headers, + ) + assert 204 == response.status_code + + response = requests.get(endpoint, auth=BearerAuth(self.access_token)) + assert 200 == response.status_code + + return self.to_azure_user(response.json()) + + def to_azure_user(self, item) -> AzureUser: + there_is_email = len(item['otherMails']) > 0 + there_is_role = ( + 'extension_1d76efa96f604499acc0c0ee116a1453_role' in item + ) + + id = item['objectId'] + name = item['displayName'] + email = item['otherMails'][0] if there_is_email else '' + role = ( + item['extension_1d76efa96f604499acc0c0ee116a1453_role'] + if there_is_role + else None + ) + return AzureUser(id, name, email, role) From bcd2564c26cac444b5625b7d1b8284853ca31275 Mon Sep 17 00:00:00 2001 From: magallegos1996 Date: Thu, 12 Nov 2020 17:47:31 -0500 Subject: [PATCH 2/7] feat: testing for update user role #229 --- .../users/users_namespace_test.py | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/tests/time_tracker_api/users/users_namespace_test.py b/tests/time_tracker_api/users/users_namespace_test.py index dfca1e93..d7e5e2fa 100644 --- a/tests/time_tracker_api/users/users_namespace_test.py +++ b/tests/time_tracker_api/users/users_namespace_test.py @@ -1,8 +1,14 @@ -from unittest.mock import Mock +from unittest.mock import Mock, patch from flask import json from flask.testing import FlaskClient from flask_restplus._http import HTTPStatus from utils.azure_users import AzureConnection +from faker import Faker + +fake = Faker() + +valid_user_role_data = {'role': 'admin'} +user_id = fake.random_int(1, 9999) def test_users_response_contains_expected_props( @@ -23,3 +29,37 @@ def test_users_response_contains_expected_props( assert 'name' in json.loads(response.data)[0] assert 'email' in json.loads(response.data)[0] assert 'role' in json.loads(response.data)[0] + + +def test_update_user_role_response_contains_expected_props( + client: FlaskClient, + valid_header: dict, +): + AzureConnection.update_user_role = Mock( + return_value=[{'name': 'dummy', 'email': 'dummy', 'role': 'dummy'}] + ) + + response = client.put( + f'/users/{user_id}', headers=valid_header, json=valid_user_role_data + ) + + assert HTTPStatus.OK == response.status_code + assert 'name' in json.loads(response.data)[0] + assert 'email' in json.loads(response.data)[0] + assert 'role' in json.loads(response.data)[0] + + +@patch('utils.azure_users.AzureConnection', new_callable=Mock) +def test_update_user_role_is_being_called_with_valid_arguments( + update_user_role_mock, + client: FlaskClient, + valid_header: dict, +): + + response = client.put( + f'/users/{user_id}', headers=valid_header, json=valid_user_role_data + ) + + assert HTTPStatus.OK == response.status_code + assert valid_user_role_data['role'] == 'admin' + update_user_role_mock.assert_called_once() From 68f4961333786769377bc7716a2ee54da89e1e85 Mon Sep 17 00:00:00 2001 From: magallegos1996 Date: Fri, 13 Nov 2020 11:34:26 -0500 Subject: [PATCH 3/7] feat: add new fixture user_id testing update user role is being called with expected arguments #229 --- tests/conftest.py | 5 +++++ .../users/users_namespace_test.py | 21 +++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3c55d3e1..e03221b9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -140,6 +140,11 @@ def owner_id() -> str: return fake.uuid4() +@pytest.fixture(scope="session") +def user_id() -> str: + return fake.uuid4() + + @pytest.fixture(scope="function") def sample_item( cosmos_db_repository: CosmosDBRepository, diff --git a/tests/time_tracker_api/users/users_namespace_test.py b/tests/time_tracker_api/users/users_namespace_test.py index d7e5e2fa..a0c66d5a 100644 --- a/tests/time_tracker_api/users/users_namespace_test.py +++ b/tests/time_tracker_api/users/users_namespace_test.py @@ -7,9 +7,6 @@ fake = Faker() -valid_user_role_data = {'role': 'admin'} -user_id = fake.random_int(1, 9999) - def test_users_response_contains_expected_props( client: FlaskClient, @@ -34,9 +31,11 @@ def test_users_response_contains_expected_props( def test_update_user_role_response_contains_expected_props( client: FlaskClient, valid_header: dict, + user_id: dict, ): + valid_user_role_data = {'role': 'admin'} AzureConnection.update_user_role = Mock( - return_value=[{'name': 'dummy', 'email': 'dummy', 'role': 'dummy'}] + return_value={'name': 'dummy', 'email': 'dummy', 'role': 'dummy'} ) response = client.put( @@ -44,22 +43,26 @@ def test_update_user_role_response_contains_expected_props( ) assert HTTPStatus.OK == response.status_code - assert 'name' in json.loads(response.data)[0] - assert 'email' in json.loads(response.data)[0] - assert 'role' in json.loads(response.data)[0] + assert 'name' in json.loads(response.data) + assert 'email' in json.loads(response.data) + assert 'role' in json.loads(response.data) -@patch('utils.azure_users.AzureConnection', new_callable=Mock) +@patch('utils.azure_users.AzureConnection.update_user_role', new_callable=Mock) def test_update_user_role_is_being_called_with_valid_arguments( update_user_role_mock, client: FlaskClient, valid_header: dict, + user_id: dict, ): + valid_user_role_data = {'role': 'admin'} response = client.put( f'/users/{user_id}', headers=valid_header, json=valid_user_role_data ) assert HTTPStatus.OK == response.status_code assert valid_user_role_data['role'] == 'admin' - update_user_role_mock.assert_called_once() + update_user_role_mock.assert_called_once_with( + user_id, valid_user_role_data['role'] + ) From 47e21af989535ce587e6af00147a64779034faa7 Mon Sep 17 00:00:00 2001 From: magallegos1996 Date: Fri, 13 Nov 2020 12:36:15 -0500 Subject: [PATCH 4/7] feat: response and user input fields now accepts a null value in their role field #229 --- tests/time_tracker_api/users/users_namespace_test.py | 8 ++------ time_tracker_api/users/users_namespace.py | 10 +++++----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/time_tracker_api/users/users_namespace_test.py b/tests/time_tracker_api/users/users_namespace_test.py index a0c66d5a..a4d49afe 100644 --- a/tests/time_tracker_api/users/users_namespace_test.py +++ b/tests/time_tracker_api/users/users_namespace_test.py @@ -3,9 +3,6 @@ from flask.testing import FlaskClient from flask_restplus._http import HTTPStatus from utils.azure_users import AzureConnection -from faker import Faker - -fake = Faker() def test_users_response_contains_expected_props( @@ -31,7 +28,7 @@ def test_users_response_contains_expected_props( def test_update_user_role_response_contains_expected_props( client: FlaskClient, valid_header: dict, - user_id: dict, + user_id: str, ): valid_user_role_data = {'role': 'admin'} AzureConnection.update_user_role = Mock( @@ -53,7 +50,7 @@ def test_update_user_role_is_being_called_with_valid_arguments( update_user_role_mock, client: FlaskClient, valid_header: dict, - user_id: dict, + user_id: str, ): valid_user_role_data = {'role': 'admin'} @@ -62,7 +59,6 @@ def test_update_user_role_is_being_called_with_valid_arguments( ) assert HTTPStatus.OK == response.status_code - assert valid_user_role_data['role'] == 'admin' update_user_role_mock.assert_called_once_with( user_id, valid_user_role_data['role'] ) diff --git a/time_tracker_api/users/users_namespace.py b/time_tracker_api/users/users_namespace.py index 7c05cf62..ed6e194b 100644 --- a/time_tracker_api/users/users_namespace.py +++ b/time_tracker_api/users/users_namespace.py @@ -2,7 +2,7 @@ from flask_restplus import fields, Resource from flask_restplus._http import HTTPStatus -from time_tracker_api.api import common_fields, api +from time_tracker_api.api import common_fields, api, NullableString faker = Faker() @@ -25,11 +25,11 @@ description='Email of the user that belongs to the tenant', example=faker.email(), ), - 'role': fields.String( + 'role': NullableString( title="User's Role", max_length=50, description='Role assigned to the user by the tenant', - example=faker.word(['admin']), + example=faker.word(['time-tracker-admin']), ), }, ) @@ -39,12 +39,12 @@ user_input_fields = ns.model( 'UserInput', { - 'role': fields.String( + 'role': NullableString( title="User's Role", required=True, max_length=50, description='Role assigned to the user by the tenant', - example=faker.word(['admin']), + example=faker.word(['time-tracker-admin']), ), }, ) From d6f5ad59dace2a5329d82e86bbf739293eb9e31d Mon Sep 17 00:00:00 2001 From: roberto Date: Mon, 16 Nov 2020 11:48:10 -0500 Subject: [PATCH 5/7] feat: update url #229 --- .../users/users_namespace_test.py | 20 +++++++++---------- time_tracker_api/users/users_namespace.py | 14 ++++--------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/tests/time_tracker_api/users/users_namespace_test.py b/tests/time_tracker_api/users/users_namespace_test.py index a4d49afe..7cbef2cb 100644 --- a/tests/time_tracker_api/users/users_namespace_test.py +++ b/tests/time_tracker_api/users/users_namespace_test.py @@ -6,18 +6,14 @@ def test_users_response_contains_expected_props( - client: FlaskClient, - valid_header: dict, + client: FlaskClient, valid_header: dict, ): AzureConnection.users = Mock( return_value=[{'name': 'dummy', 'email': 'dummy', 'role': 'dummy'}] ) - response = client.get( - '/users', - headers=valid_header, - ) + response = client.get('/users', headers=valid_header,) assert HTTPStatus.OK == response.status_code assert 'name' in json.loads(response.data)[0] @@ -26,9 +22,7 @@ def test_users_response_contains_expected_props( def test_update_user_role_response_contains_expected_props( - client: FlaskClient, - valid_header: dict, - user_id: str, + client: FlaskClient, valid_header: dict, user_id: str, ): valid_user_role_data = {'role': 'admin'} AzureConnection.update_user_role = Mock( @@ -36,7 +30,9 @@ def test_update_user_role_response_contains_expected_props( ) response = client.put( - f'/users/{user_id}', headers=valid_header, json=valid_user_role_data + f'/users/{user_id}/roles', + headers=valid_header, + json=valid_user_role_data, ) assert HTTPStatus.OK == response.status_code @@ -55,7 +51,9 @@ def test_update_user_role_is_being_called_with_valid_arguments( valid_user_role_data = {'role': 'admin'} response = client.put( - f'/users/{user_id}', headers=valid_header, json=valid_user_role_data + f'/users/{user_id}/roles', + headers=valid_header, + json=valid_user_role_data, ) assert HTTPStatus.OK == response.status_code diff --git a/time_tracker_api/users/users_namespace.py b/time_tracker_api/users/users_namespace.py index ed6e194b..b3372a31 100644 --- a/time_tracker_api/users/users_namespace.py +++ b/time_tracker_api/users/users_namespace.py @@ -62,25 +62,19 @@ def get(self): return azure_connection.users() -@ns.route('/') +@ns.route('//roles') @ns.response(HTTPStatus.NOT_FOUND, 'User not found') @ns.response(HTTPStatus.UNPROCESSABLE_ENTITY, 'The id has an invalid format') @ns.param('id', 'The user identifier') -class User(Resource): - @ns.doc('get_user') - @ns.marshal_with(user_response_fields) - def get(self, id): - """Get an user""" - return {} - - @ns.doc('update_user') +class UserRole(Resource): + @ns.doc('update_user_role') @ns.expect(user_input_fields) @ns.response( HTTPStatus.BAD_REQUEST, 'Invalid format or structure of the user' ) @ns.marshal_with(user_response_fields) def put(self, id): - """Update an user""" + """Update user's role""" from utils.azure_users import AzureConnection azure_connection = AzureConnection() From da5024fd56044f92f7ed89cea29a86e857491781 Mon Sep 17 00:00:00 2001 From: roberto Date: Tue, 17 Nov 2020 11:30:13 -0500 Subject: [PATCH 6/7] feat: move extension_role to variable #229 --- utils/azure_users.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/utils/azure_users.py b/utils/azure_users.py index 21c3f61e..15ec85b6 100644 --- a/utils/azure_users.py +++ b/utils/azure_users.py @@ -55,6 +55,8 @@ def __init__(self, config=MSConfig): self.config = config self.access_token = self.get_token() + self.role_field = 'extension_1d76efa96f604499acc0c0ee116a1453_role' + def get_token(self): response = self.client.acquire_token_for_client( scopes=self.config.SCOPE @@ -67,8 +69,7 @@ def get_token(self): def users(self) -> List[AzureUser]: endpoint = "{endpoint}/users?api-version=1.6&$select=displayName,otherMails,objectId,{role_field}".format( - endpoint=self.config.ENDPOINT, - role_field='extension_1d76efa96f604499acc0c0ee116a1453_role', + endpoint=self.config.ENDPOINT, role_field=self.role_field, ) response = requests.get(endpoint, auth=BearerAuth(self.access_token)) @@ -84,7 +85,7 @@ def update_user_role(self, id, role): endpoint = "{endpoint}/users/{user_id}?api-version=1.6".format( endpoint=self.config.ENDPOINT, user_id=id ) - data = {'extension_1d76efa96f604499acc0c0ee116a1453_role': role} + data = {self.role_field: role} response = requests.patch( endpoint, auth=BearerAuth(self.access_token), @@ -100,16 +101,10 @@ def update_user_role(self, id, role): def to_azure_user(self, item) -> AzureUser: there_is_email = len(item['otherMails']) > 0 - there_is_role = ( - 'extension_1d76efa96f604499acc0c0ee116a1453_role' in item - ) + there_is_role = self.role_field in item id = item['objectId'] name = item['displayName'] email = item['otherMails'][0] if there_is_email else '' - role = ( - item['extension_1d76efa96f604499acc0c0ee116a1453_role'] - if there_is_role - else None - ) + role = item[self.role_field] if there_is_role else None return AzureUser(id, name, email, role) From 67ce20bff82e235fc3f9e18278d99231267ab5b8 Mon Sep 17 00:00:00 2001 From: roberto Date: Thu, 19 Nov 2020 12:41:28 -0500 Subject: [PATCH 7/7] feat: change PUT for POST and DELETE #229 --- .../users/users_namespace_test.py | 22 +++++++-- time_tracker_api/users/users_namespace.py | 46 +++++++++++-------- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/tests/time_tracker_api/users/users_namespace_test.py b/tests/time_tracker_api/users/users_namespace_test.py index 7cbef2cb..46ff68fa 100644 --- a/tests/time_tracker_api/users/users_namespace_test.py +++ b/tests/time_tracker_api/users/users_namespace_test.py @@ -29,7 +29,7 @@ def test_update_user_role_response_contains_expected_props( return_value={'name': 'dummy', 'email': 'dummy', 'role': 'dummy'} ) - response = client.put( + response = client.post( f'/users/{user_id}/roles', headers=valid_header, json=valid_user_role_data, @@ -42,7 +42,7 @@ def test_update_user_role_response_contains_expected_props( @patch('utils.azure_users.AzureConnection.update_user_role', new_callable=Mock) -def test_update_user_role_is_being_called_with_valid_arguments( +def test_on_post_update_user_role_is_being_called_with_valid_arguments( update_user_role_mock, client: FlaskClient, valid_header: dict, @@ -50,7 +50,7 @@ def test_update_user_role_is_being_called_with_valid_arguments( ): valid_user_role_data = {'role': 'admin'} - response = client.put( + response = client.post( f'/users/{user_id}/roles', headers=valid_header, json=valid_user_role_data, @@ -60,3 +60,19 @@ def test_update_user_role_is_being_called_with_valid_arguments( update_user_role_mock.assert_called_once_with( user_id, valid_user_role_data['role'] ) + + +@patch('utils.azure_users.AzureConnection.update_user_role', new_callable=Mock) +def test_on_delete_update_user_role_is_being_called_with_valid_arguments( + update_user_role_mock, + client: FlaskClient, + valid_header: dict, + user_id: str, +): + + response = client.delete( + f'/users/{user_id}/roles/time-tracker-admin', headers=valid_header, + ) + + assert HTTPStatus.OK == response.status_code + update_user_role_mock.assert_called_once_with(user_id, role=None) diff --git a/time_tracker_api/users/users_namespace.py b/time_tracker_api/users/users_namespace.py index b3372a31..fea40834 100644 --- a/time_tracker_api/users/users_namespace.py +++ b/time_tracker_api/users/users_namespace.py @@ -4,7 +4,10 @@ from time_tracker_api.api import common_fields, api, NullableString -faker = Faker() +from utils.azure_users import AzureConnection + + +azure_connection = AzureConnection() ns = api.namespace('users', description='Namespace of the API for users') @@ -17,34 +20,34 @@ title='Name', max_length=50, description='Name of the user', - example=faker.word(['Marcelo', 'Sandro']), + example=Faker().word(['Marcelo', 'Sandro']), ), 'email': fields.String( title="User's Email", max_length=50, description='Email of the user that belongs to the tenant', - example=faker.email(), + example=Faker().email(), ), 'role': NullableString( title="User's Role", max_length=50, description='Role assigned to the user by the tenant', - example=faker.word(['time-tracker-admin']), + example=Faker().word(['time-tracker-admin']), ), }, ) user_response_fields.update(common_fields) -user_input_fields = ns.model( - 'UserInput', +user_role_input_fields = ns.model( + 'UserRoleInput', { 'role': NullableString( title="User's Role", required=True, max_length=50, description='Role assigned to the user by the tenant', - example=faker.word(['time-tracker-admin']), + example=Faker().word(['time-tracker-admin']), ), }, ) @@ -56,9 +59,6 @@ class Users(Resource): @ns.marshal_list_with(user_response_fields) def get(self): """List all users""" - from utils.azure_users import AzureConnection - - azure_connection = AzureConnection() return azure_connection.users() @@ -66,16 +66,26 @@ def get(self): @ns.response(HTTPStatus.NOT_FOUND, 'User not found') @ns.response(HTTPStatus.UNPROCESSABLE_ENTITY, 'The id has an invalid format') @ns.param('id', 'The user identifier') -class UserRole(Resource): - @ns.doc('update_user_role') - @ns.expect(user_input_fields) +class UserRoles(Resource): + @ns.doc('create_user_role') + @ns.expect(user_role_input_fields) @ns.response( HTTPStatus.BAD_REQUEST, 'Invalid format or structure of the user' ) @ns.marshal_with(user_response_fields) - def put(self, id): - """Update user's role""" - from utils.azure_users import AzureConnection - - azure_connection = AzureConnection() + def post(self, id): + """Create user's role""" return azure_connection.update_user_role(id, ns.payload['role']) + + +@ns.route('//roles/') +@ns.response(HTTPStatus.NOT_FOUND, 'User not found') +@ns.response(HTTPStatus.UNPROCESSABLE_ENTITY, 'The id has an invalid format') +@ns.param('user_id', 'The user identifier') +@ns.param('role_id', 'The role name identifier') +class UserRole(Resource): + @ns.doc('delete_user_role') + @ns.marshal_with(user_response_fields) + def delete(self, user_id, role_id): + """Delete user's role""" + return azure_connection.update_user_role(user_id, role=None)