diff --git a/.github/ISSUE_TEMPLATE/back-end.md b/.github/ISSUE_TEMPLATE/back-end.md new file mode 100644 index 00000000..e4aaa071 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/back-end.md @@ -0,0 +1,29 @@ +--- +name: Back end +about: new tasks of Back-end +title: BACK-END +labels: mvp, nice-to-have, tech-debt, enhancement +assignees: '' + +--- + +**Purpose** +A clear and concise description of this new task or feature + +** Purpose** +A clear and concise description of what you want to happen. + +**Optional diagrams** +Optional designs of diagrams. + +**Criteria of acceptance** +List the parameters that the task must meet to be approved. +- parameter one +- parameter two +- ... + +**Scope** +Defined scope of this task, for example business logic, the recommendation is to create a meeting with more than one person to define this scope. + +**Note** +Information adicional, for example links of documentation to work in xz task. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..ea197258 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: BUG +labels: low, medium, high +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/CHANGELOG.md b/CHANGELOG.md index 956d2902..d408f7e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ +## v0.24.4 (2020-11-14) +### Fix +* Delete ui-task template ([`0be1370`](https://github.com/ioet/time-tracker-backend/commit/0be1370d81f4dcec739ed2bf8f3eff25640af36b)) +* Issue templates ([`55b73bb`](https://github.com/ioet/time-tracker-backend/commit/55b73bb9cc546306f2c81785581fd593d71172fe)) +* Issue templates ([`76728f4`](https://github.com/ioet/time-tracker-backend/commit/76728f491820aa95d1f45ddb2565bd05ed34648f)) + ## v0.24.3 (2020-11-10) ### Fix * Allow limits overlap in time entries #217 ([`ce99603`](https://github.com/ioet/time-tracker-backend/commit/ce996032bf4cf20c129b84ad293db4c031571c95)) diff --git a/tests/conftest.py b/tests/conftest.py index 5cb5c18d..6ecfb5be 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -84,7 +84,11 @@ def cosmos_db_model(): return { 'id': 'test', 'partition_key': PartitionKey(path='/tenant_id'), - 'unique_key_policy': {'uniqueKeys': [{'paths': ['/email']},]}, + 'unique_key_policy': { + 'uniqueKeys': [ + {'paths': ['/email']}, + ] + }, } @@ -136,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..7cbef2cb 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,57 @@ 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.put( + 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_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.put( + 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'] + ) diff --git a/time_tracker_api/users/users_namespace.py b/time_tracker_api/users/users_namespace.py index 80d40139..b3372a31 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,17 +25,30 @@ 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']), ), }, ) user_response_fields.update(common_fields) +user_input_fields = ns.model( + 'UserInput', + { + '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): @@ -49,13 +62,20 @@ 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') +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 get(self, id): - """Get an user""" - return {} + def put(self, id): + """Update user's role""" + from utils.azure_users import AzureConnection + + azure_connection = AzureConnection() + return azure_connection.update_user_role(id, ns.payload['role']) diff --git a/time_tracker_api/version.py b/time_tracker_api/version.py index c6d6a56b..1f4f7fb6 100644 --- a/time_tracker_api/version.py +++ b/time_tracker_api/version.py @@ -1 +1 @@ -__version__ = '0.24.3' +__version__ = '0.24.4' 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)