22# Copyright (c) Microsoft Corporation. All rights reserved.
33# Licensed under the MIT License. See License.txt in the project root for license information.
44# --------------------------------------------------------------------------------------------
5+ import uuid
56import pytest
67from unittest import mock
78
8- from azure .cli .command_modules .role .custom import _resolve_role_id , _search_role_assignments
9+ from azure .cli .command_modules .role .custom import (
10+ _resolve_role_id , _search_role_assignments , _get_role_definition_id
11+ )
912
1013# pylint: disable=line-too-long
1114
1215
16+ class TestGetRoleDefinitionId :
17+ """Tests for _get_role_definition_id helper function."""
18+
19+ @pytest .mark .parametrize ("resource_id,expected" , [
20+ # Tenant-scoped format
21+ ('/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7' ,
22+ uuid .UUID ('acdd72a7-3385-48ef-bd42-f606fba81ae7' )),
23+ # Subscription-scoped format
24+ ('/subscriptions/sub1/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c' ,
25+ uuid .UUID ('b24988ac-6180-42a0-ab88-20f7382dd24c' )),
26+ # None returns None
27+ (None , None ),
28+ # Empty string returns None (not a valid GUID)
29+ ('' , None ),
30+ # Invalid GUID in last segment returns None
31+ ('/providers/Microsoft.Authorization/roleDefinitions/not-a-guid' , None ),
32+ # Malformed path with extra segments still extracts last segment if valid GUID
33+ ('/some/path/acdd72a7-3385-48ef-bd42-f606fba81ae7' ,
34+ uuid .UUID ('acdd72a7-3385-48ef-bd42-f606fba81ae7' )),
35+ ])
36+ def test_extracts_guid_from_role_definition_id (self , resource_id , expected ):
37+ """Extracts the GUID from various role definition ID formats."""
38+ result = _get_role_definition_id (resource_id )
39+ assert result == expected
40+
41+ def test_returns_uuid_for_case_insensitive_comparison (self ):
42+ """Returns UUID objects that compare equal regardless of case."""
43+ lower = _get_role_definition_id ('/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7' )
44+ upper = _get_role_definition_id ('/providers/Microsoft.Authorization/roleDefinitions/ACDD72A7-3385-48EF-BD42-F606FBA81AE7' )
45+ assert lower == upper
46+ assert isinstance (lower , uuid .UUID )
47+ assert isinstance (upper , uuid .UUID )
48+
49+
1350class TestResolveRoleId :
1451 """Tests for _resolve_role_id function."""
1552
@@ -80,7 +117,7 @@ def _create_assignment(scope, role_definition_id, principal_id='principal-1'):
80117 @pytest .mark .parametrize ("scope,role_def_format" , [
81118 # Root scope with tenant-format role definition ID
82119 ('/' , '/providers/Microsoft.Authorization/roleDefinitions/{guid}' ),
83- # Management group scope with tenant-format role definition ID
120+ # Management group scope with tenant-format role definition ID
84121 ('/providers/Microsoft.Management/managementGroups/my-mg' ,
85122 '/providers/Microsoft.Authorization/roleDefinitions/{guid}' ),
86123 # Subscription scope with subscription-format role definition ID
@@ -123,104 +160,6 @@ def test_different_role_guid_does_not_match(self, mock_clients):
123160
124161 assert len (result ) == 0
125162
126- def test_scope_comparison_is_case_insensitive (self , mock_clients ):
127- """Scope matching is case insensitive."""
128- assignments_client , definitions_client = mock_clients
129- role_def_id = '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
130-
131- assignments_client .list_for_scope .return_value = [
132- self ._create_assignment ('/Subscriptions/SUB1/ResourceGroups/RG1' , role_def_id ),
133- ]
134-
135- result = _search_role_assignments (
136- assignments_client , definitions_client ,
137- scope = '/subscriptions/sub1/resourcegroups/rg1' ,
138- assignee_object_id = None , role = None ,
139- include_inherited = False , include_groups = False
140- )
141-
142- assert len (result ) == 1
143-
144- def test_include_inherited_returns_parent_scope_assignments (self , mock_clients ):
145- """include_inherited=True returns assignments at and above the scope."""
146- assignments_client , definitions_client = mock_clients
147- role_def_id = '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
148-
149- assignments_client .list_for_scope .return_value = [
150- self ._create_assignment ('/' , role_def_id , 'principal-1' ),
151- self ._create_assignment ('/subscriptions/sub1' , role_def_id , 'principal-2' ),
152- ]
153-
154- result = _search_role_assignments (
155- assignments_client , definitions_client ,
156- scope = '/subscriptions/sub1' ,
157- assignee_object_id = None , role = None ,
158- include_inherited = True , include_groups = False
159- )
160-
161- assert len (result ) == 2
162-
163- def test_include_inherited_false_filters_parent_scope (self , mock_clients ):
164- """include_inherited=False filters out assignments at parent scopes."""
165- assignments_client , definitions_client = mock_clients
166- role_def_id = '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
167-
168- assignments_client .list_for_scope .return_value = [
169- self ._create_assignment ('/' , role_def_id , 'principal-1' ),
170- self ._create_assignment ('/subscriptions/sub1' , role_def_id , 'principal-2' ),
171- ]
172-
173- result = _search_role_assignments (
174- assignments_client , definitions_client ,
175- scope = '/subscriptions/sub1' ,
176- assignee_object_id = None , role = None ,
177- include_inherited = False , include_groups = False
178- )
179-
180- assert len (result ) == 1
181- assert result [0 ].principal_id == 'principal-2'
182-
183- def test_assignee_filter (self , mock_clients ):
184- """assignee_object_id filters assignments by principal."""
185- assignments_client , definitions_client = mock_clients
186- role_def_id = '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
187-
188- assignments_client .list_for_scope .return_value = [
189- self ._create_assignment ('/subscriptions/sub1' , role_def_id , 'principal-1' ),
190- self ._create_assignment ('/subscriptions/sub1' , role_def_id , 'principal-2' ),
191- ]
192-
193- result = _search_role_assignments (
194- assignments_client , definitions_client ,
195- scope = '/subscriptions/sub1' ,
196- assignee_object_id = 'principal-1' , role = None ,
197- include_inherited = False , include_groups = False
198- )
199-
200- assert len (result ) == 1
201- assert result [0 ].principal_id == 'principal-1'
202-
203- def test_no_scope_uses_subscription_api (self , mock_clients ):
204- """When scope is None, list_for_subscription is called and all assignments returned."""
205- assignments_client , definitions_client = mock_clients
206- role_def_id = '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
207-
208- assignments_client .list_for_subscription .return_value = [
209- self ._create_assignment ('/subscriptions/sub1' , role_def_id , 'principal-1' ),
210- self ._create_assignment ('/subscriptions/sub1/resourceGroups/rg1' , role_def_id , 'principal-2' ),
211- ]
212-
213- result = _search_role_assignments (
214- assignments_client , definitions_client ,
215- scope = None ,
216- assignee_object_id = None , role = None ,
217- include_inherited = False , include_groups = False
218- )
219-
220- assert len (result ) == 2
221- assignments_client .list_for_subscription .assert_called_once ()
222- assignments_client .list_for_scope .assert_not_called ()
223-
224163 def test_none_role_definition_id_is_skipped (self , mock_clients ):
225164 """Assignments with None role_definition_id are skipped when filtering by role."""
226165 assignments_client , definitions_client = mock_clients
@@ -238,4 +177,4 @@ def test_none_role_definition_id_is_skipped(self, mock_clients):
238177 )
239178
240179 assert len (result ) == 1
241- assert result [0 ].role_definition_id is not None
180+ assert result [0 ].role_definition_id is not None
0 commit comments