Skip to content

Commit 4c6f8ba

Browse files
authored
feat: TT-156 add groups field in User model (#262)
* feat: TT-156 add group fields in model * feat: TT-156 cache groups_and_users response for perfomance * test: TT-156 update tests * test: TT-156 add get_grups_and_users test * fix: TT-156 remove debug statements * test: TT-156 add test for get_groups_by_user_id * test: TT-156 parametrize test for get_groups_by_user_id * test: TT-156 get_groups_and_users_called_once_by_instance
1 parent 87894dd commit 4c6f8ba

File tree

4 files changed

+141
-29
lines changed

4 files changed

+141
-29
lines changed

tests/time_tracker_api/users/users_namespace_test.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@
1010
@patch('utils.azure_users.AzureConnection.get_token', Mock())
1111
@patch('utils.azure_users.AzureConnection.get_user')
1212
def test_get_user_response_contains_expected_props(
13-
get_user_mock,
14-
client: FlaskClient,
15-
valid_header: dict,
13+
get_user_mock, client: FlaskClient, valid_header: dict,
1614
):
1715
get_user_mock.return_value = {
1816
'name': 'dummy',
1917
'email': 'dummy',
2018
'roles': ['dummy-role'],
19+
'groups': ['dummy-group'],
2120
}
2221
user_id = (Faker().uuid4(),)
2322
response = client.get(f'/users/{user_id}', headers=valid_header)
@@ -27,7 +26,9 @@ def test_get_user_response_contains_expected_props(
2726
assert 'name' in json.loads(response.data)
2827
assert 'email' in json.loads(response.data)
2928
assert 'roles' in json.loads(response.data)
29+
assert 'groups' in json.loads(response.data)
3030
assert ['dummy-role'] == json.loads(response.data)['roles']
31+
assert ['dummy-group'] == json.loads(response.data)['groups']
3132

3233

3334
@patch('utils.azure_users.AzureConnection.get_msal_client', Mock())
@@ -37,12 +38,15 @@ def test_get_user_response_contains_expected_props(
3738
)
3839
@patch('utils.azure_users.AzureConnection.users')
3940
def test_users_response_contains_expected_props(
40-
users_mock,
41-
client: FlaskClient,
42-
valid_header: dict,
41+
users_mock, client: FlaskClient, valid_header: dict,
4342
):
4443
users_mock.return_value = [
45-
{'name': 'dummy', 'email': 'dummy', 'roles': ['dummy-role']}
44+
{
45+
'name': 'dummy',
46+
'email': 'dummy',
47+
'roles': ['dummy-role'],
48+
'groups': ['dummy-group'],
49+
}
4650
]
4751
response = client.get('/users', headers=valid_header)
4852

@@ -51,15 +55,16 @@ def test_users_response_contains_expected_props(
5155
assert 'name' in json.loads(response.data)[0]
5256
assert 'email' in json.loads(response.data)[0]
5357
assert 'roles' in json.loads(response.data)[0]
58+
assert 'groups' in json.loads(response.data)[0]
5459
assert ['dummy-role'] == json.loads(response.data)[0]['roles']
60+
assert ['dummy-group'] == json.loads(response.data)[0]['groups']
5561

5662

5763
@patch('utils.azure_users.AzureConnection.get_msal_client', Mock())
5864
@patch('utils.azure_users.AzureConnection.get_token', Mock())
5965
@patch('utils.azure_users.AzureConnection.update_role')
6066
@mark.parametrize(
61-
'role_id,action',
62-
[('test', 'grant'), ('admin', 'revoke')],
67+
'role_id,action', [('test', 'grant'), ('admin', 'revoke')],
6368
)
6469
def test_update_role_response_contains_expected_props(
6570
update_role_mock,
@@ -73,15 +78,16 @@ def test_update_role_response_contains_expected_props(
7378
'name': 'dummy',
7479
'email': 'dummy',
7580
'roles': [],
81+
'groups': [],
7682
}
7783
response = client.post(
78-
f'/users/{user_id}/roles/{role_id}/{action}',
79-
headers=valid_header,
84+
f'/users/{user_id}/roles/{role_id}/{action}', headers=valid_header,
8085
)
8186
assert HTTPStatus.OK == response.status_code
8287
assert 'name' in json.loads(response.data)
8388
assert 'email' in json.loads(response.data)
8489
assert 'roles' in json.loads(response.data)
90+
assert 'groups' in json.loads(response.data)
8591

8692

8793
@patch('utils.azure_users.AzureConnection.get_msal_client', Mock())
@@ -107,8 +113,7 @@ def test_update_role_is_called_properly_on_each_action(
107113
):
108114
update_role_mock.return_value = {}
109115
response = client.post(
110-
f'/users/{user_id}/roles/{role_id}/{action}',
111-
headers=valid_header,
116+
f'/users/{user_id}/roles/{role_id}/{action}', headers=valid_header,
112117
)
113118

114119
assert HTTPStatus.OK == response.status_code

tests/utils/azure_users_test.py

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@
1414
],
1515
)
1616
def test_azure_connection_is_test_user(
17-
get_mock,
18-
field_name,
19-
field_value,
20-
is_test_user_expected_value,
17+
get_mock, field_name, field_value, is_test_user_expected_value,
2118
):
2219
response_mock = Mock()
2320
response_mock.status_code = 200
@@ -36,12 +33,7 @@ def test_azure_connection_get_test_user_ids(get_mock):
3633
response_mock = Mock()
3734
response_mock.status_code = 200
3835
response_mock.json = Mock(
39-
return_value={
40-
'value': [
41-
{'objectId': 'ID1'},
42-
{'objectId': 'ID2'},
43-
]
44-
}
36+
return_value={'value': [{'objectId': 'ID1'}, {'objectId': 'ID2'},]}
4537
)
4638
get_mock.return_value = response_mock
4739

@@ -57,8 +49,8 @@ def test_azure_connection_get_test_user_ids(get_mock):
5749
def test_azure_connection_get_non_test_users(
5850
users_mock, get_test_user_ids_mock
5951
):
60-
test_user = AzureUser('ID1', None, None, [])
61-
non_test_user = AzureUser('ID2', None, None, [])
52+
test_user = AzureUser('ID1', None, None, [], [])
53+
non_test_user = AzureUser('ID2', None, None, [], [])
6254
users_mock.return_value = [test_user, non_test_user]
6355
get_test_user_ids_mock.return_value = ['ID1']
6456
non_test_users = [non_test_user]
@@ -104,3 +96,83 @@ def test_is_user_in_group(
10496
azure_connection.is_user_in_group('user_id', payload_mock)
10597
== response_expected
10698
)
99+
100+
101+
@patch('utils.azure_users.AzureConnection.get_msal_client', Mock())
102+
@patch('utils.azure_users.AzureConnection.get_token', Mock())
103+
@patch('requests.get')
104+
def test_get_groups_and_users(get_mock):
105+
response_mock = Mock()
106+
response_mock.status_code = 200
107+
return_value = {
108+
'value': [
109+
{
110+
'displayName': 'test-group-1',
111+
'members': [
112+
{'objectId': 'user-id1'},
113+
{'objectId': 'user-id2'},
114+
],
115+
},
116+
{
117+
'displayName': 'test-group-2',
118+
'members': [
119+
{'objectId': 'user-id3'},
120+
{'objectId': 'user-id1'},
121+
],
122+
},
123+
{'displayName': 'test-group-3', 'members': [],},
124+
]
125+
}
126+
response_mock.json = Mock(return_value=return_value)
127+
get_mock.return_value = response_mock
128+
129+
expected_result = [
130+
('test-group-1', ['user-id1', 'user-id2']),
131+
('test-group-2', ['user-id3', 'user-id1']),
132+
('test-group-3', []),
133+
]
134+
135+
azure_connection = AzureConnection()
136+
137+
assert azure_connection.get_groups_and_users() == expected_result
138+
139+
140+
@patch('utils.azure_users.AzureConnection.get_msal_client', Mock())
141+
@patch('utils.azure_users.AzureConnection.get_token', Mock())
142+
@patch('utils.azure_users.AzureConnection.get_groups_and_users')
143+
@mark.parametrize(
144+
'user_id,groups_expected_value',
145+
[
146+
('user-id1', ['test-group-1', 'test-group-2']),
147+
('user-id2', ['test-group-1']),
148+
('user-id3', ['test-group-2']),
149+
('user-id4', []),
150+
],
151+
)
152+
def test_get_groups_by_user_id(
153+
get_groups_and_users_mock, user_id, groups_expected_value
154+
):
155+
get_groups_and_users_mock.return_value = [
156+
('test-group-1', ['user-id1', 'user-id2']),
157+
('test-group-2', ['user-id3', 'user-id1']),
158+
]
159+
160+
azure_connection = AzureConnection()
161+
groups = azure_connection.get_groups_by_user_id(user_id)
162+
assert groups == groups_expected_value
163+
164+
165+
@patch('utils.azure_users.AzureConnection.get_msal_client', Mock())
166+
@patch('utils.azure_users.AzureConnection.get_token', Mock())
167+
@patch('utils.azure_users.AzureConnection.get_groups_and_users')
168+
def test_get_groups_and_users_called_once_by_instance(
169+
get_groups_and_users_mock,
170+
):
171+
get_groups_and_users_mock.return_value = []
172+
user_id = 'user-id1'
173+
azure_connection = AzureConnection()
174+
azure_connection.get_groups_by_user_id(user_id)
175+
azure_connection.get_groups_by_user_id(user_id)
176+
azure_connection.get_groups_by_user_id(user_id)
177+
178+
get_groups_and_users_mock.assert_called_once()

time_tracker_api/users/users_namespace.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,23 @@
2929
title='Roles',
3030
description='List of the roles assigned to the user by the tenant',
3131
),
32+
example=Faker().words(
33+
3, ['time-tracker-admin', 'test-user', 'guest',], unique=True
34+
),
35+
),
36+
'groups': fields.List(
37+
fields.String(
38+
title='Groups',
39+
description='List of the groups the user belongs to, assigned by the tenant',
40+
),
3241
example=Faker().words(
3342
3,
3443
[
3544
'time-tracker-admin',
36-
'test-user',
37-
'guest',
45+
'time-tracker-tester',
46+
'time-tracker-guest',
3847
],
48+
unique=True,
3949
),
4050
),
4151
},

utils/azure_users.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ def __call__(self, r):
3434

3535

3636
class AzureUser:
37-
def __init__(self, id, name, email, roles):
37+
def __init__(self, id, name, email, roles, groups):
3838
self.id = id
3939
self.name = name
4040
self.email = email
4141
self.roles = roles
42+
self.groups = groups
4243

4344

4445
HTTP_PATCH_HEADERS = {
@@ -63,6 +64,7 @@ def __init__(self, config=MSConfig):
6364
self.config = config
6465
self.client = self.get_msal_client()
6566
self.access_token = self.get_token()
67+
self.groups_and_users = None
6668

6769
def get_msal_client(self):
6870
client = msal.ConfidentialClientApplication(
@@ -115,7 +117,8 @@ def to_azure_user(self, item) -> AzureUser:
115117
for (field_name, field_value) in ROLE_FIELD_VALUES.values()
116118
if field_name in item
117119
]
118-
return AzureUser(id, name, email, roles)
120+
groups = self.get_groups_by_user_id(id)
121+
return AzureUser(id, name, email, roles, groups)
119122

120123
def update_role(self, user_id, role_id, is_grant):
121124
endpoint = "{endpoint}/users/{user_id}?api-version=1.6".format(
@@ -181,6 +184,28 @@ def get_group_id_by_group_name(self, group_name):
181184

182185
return response.json()['value'][0]['objectId']
183186

187+
def get_groups_by_user_id(self, user_id):
188+
if self.groups_and_users is None:
189+
self.groups_and_users = self.get_groups_and_users()
190+
return [
191+
group_name
192+
for (group_name, user_ids) in self.groups_and_users
193+
if user_id in user_ids
194+
]
195+
196+
def get_groups_and_users(self):
197+
endpoint = "{endpoint}/groups?api-version=1.6&$select=displayName,members&$expand=members".format(
198+
endpoint=self.config.ENDPOINT
199+
)
200+
response = requests.get(endpoint, auth=BearerAuth(self.access_token))
201+
assert 200 == response.status_code
202+
parse_item = lambda item: (
203+
item['displayName'],
204+
[member['objectId'] for member in item['members']],
205+
)
206+
result = list(map(parse_item, response.json()['value']))
207+
return result
208+
184209
def is_user_in_group(self, user_id, data: dict):
185210
group_id = self.get_group_id_by_group_name(
186211
group_name=data['group_name']

0 commit comments

Comments
 (0)