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 dfca1e93..46ff68fa 100644 --- a/tests/time_tracker_api/users/users_namespace_test.py +++ b/tests/time_tracker_api/users/users_namespace_test.py @@ -1,4 +1,4 @@ -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 @@ -6,20 +6,73 @@ 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] 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, user_id: str, +): + valid_user_role_data = {'role': 'admin'} + AzureConnection.update_user_role = Mock( + return_value={'name': 'dummy', 'email': 'dummy', 'role': 'dummy'} + ) + + response = client.post( + f'/users/{user_id}/roles', + headers=valid_header, + json=valid_user_role_data, + ) + + assert HTTPStatus.OK == response.status_code + 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.update_user_role', new_callable=Mock) +def test_on_post_update_user_role_is_being_called_with_valid_arguments( + update_user_role_mock, + client: FlaskClient, + valid_header: dict, + user_id: str, +): + + valid_user_role_data = {'role': 'admin'} + response = client.post( + f'/users/{user_id}/roles', + headers=valid_header, + json=valid_user_role_data, + ) + + assert HTTPStatus.OK == response.status_code + 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 80d40139..fea40834 100644 --- a/time_tracker_api/users/users_namespace.py +++ b/time_tracker_api/users/users_namespace.py @@ -2,9 +2,12 @@ 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() +from utils.azure_users import AzureConnection + + +azure_connection = AzureConnection() ns = api.namespace('users', description='Namespace of the API for users') @@ -17,25 +20,38 @@ 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': 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']), ), }, ) user_response_fields.update(common_fields) +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']), + ), + }, +) + @ns.route('') class Users(Resource): @@ -43,19 +59,33 @@ 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() -@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') +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 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 get(self, id): - """Get an user""" - return {} + def delete(self, user_id, role_id): + """Delete user's role""" + return azure_connection.update_user_role(user_id, role=None) diff --git a/utils/azure_users.py b/utils/azure_users.py index 700b654f..15ec85b6 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 @@ -54,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 @@ -65,28 +68,43 @@ 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', + endpoint=self.config.ENDPOINT, role_field=self.role_field, ) response = requests.get(endpoint, auth=BearerAuth(self.access_token)) 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 = {self.role_field: 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 = self.role_field in item + + id = item['objectId'] + name = item['displayName'] + email = item['otherMails'][0] if there_is_email else '' + role = item[self.role_field] if there_is_role else None + return AzureUser(id, name, email, role)