|
2 | 2 | # Copyright (c) Microsoft Corporation. All rights reserved. |
3 | 3 | # Licensed under the MIT License. See License.txt in the project root for license information. |
4 | 4 | # -------------------------------------------------------------------------------------------- |
5 | | -import unittest |
| 5 | +import pytest |
6 | 6 | from unittest import mock |
7 | 7 |
|
8 | | -from azure.cli.command_modules.role.custom import _resolve_role_id |
| 8 | +from azure.cli.command_modules.role.custom import _resolve_role_id, _search_role_assignments |
9 | 9 |
|
10 | 10 | # pylint: disable=line-too-long |
11 | 11 |
|
12 | 12 |
|
13 | | -class TestRoleCustomCommands(unittest.TestCase): |
| 13 | +class TestResolveRoleId: |
| 14 | + """Tests for _resolve_role_id function.""" |
14 | 15 |
|
15 | | - def test_resolve_role_id(self, ): |
16 | | - mock_client = mock.Mock() |
17 | | - mock_client._config.subscription_id = '123' |
18 | | - test_role_id = 'b24988ac-6180-42a0-ab88-20f738123456' |
| 16 | + @pytest.fixture |
| 17 | + def mock_client(self): |
| 18 | + client = mock.Mock() |
| 19 | + client._config.subscription_id = '00000000-0000-0000-0000-000000000000' |
| 20 | + return client |
19 | 21 |
|
20 | | - # action(using a logical name) |
21 | | - result = _resolve_role_id(test_role_id, 'foobar', mock_client) |
| 22 | + @pytest.mark.parametrize("role_input,expected_output", [ |
| 23 | + # GUID returns tenant format |
| 24 | + ('b24988ac-6180-42a0-ab88-20f738123456', |
| 25 | + '/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f738123456'), |
| 26 | + # Subscription-scoped ID returned as-is |
| 27 | + ('/subscriptions/sub1/providers/Microsoft.Authorization/roleDefinitions/5370bbf4-6b73-4417-969b-8f2e6e123456', |
| 28 | + '/subscriptions/sub1/providers/Microsoft.Authorization/roleDefinitions/5370bbf4-6b73-4417-969b-8f2e6e123456'), |
| 29 | + # Tenant-scoped ID returned as-is |
| 30 | + ('/providers/Microsoft.Authorization/roleDefinitions/5370bbf4-6b73-4417-969b-8f2e6e123456', |
| 31 | + '/providers/Microsoft.Authorization/roleDefinitions/5370bbf4-6b73-4417-969b-8f2e6e123456'), |
| 32 | + ]) |
| 33 | + def test_resolve_role_id_formats(self, mock_client, role_input, expected_output): |
| 34 | + """Role IDs (GUID, subscription-scoped, tenant-scoped) are resolved correctly.""" |
| 35 | + result = _resolve_role_id(role_input, '/subscriptions/sub1', mock_client) |
| 36 | + assert result == expected_output |
22 | 37 |
|
23 | | - # assert |
24 | | - self.assertEqual('/subscriptions/123/providers/Microsoft.Authorization/roleDefinitions/{}'.format(test_role_id), result) |
| 38 | + def test_role_name_queries_api(self, mock_client): |
| 39 | + """Role name triggers API lookup and returns the role definition ID from API.""" |
| 40 | + mock_role_def = mock.Mock() |
| 41 | + mock_role_def.id = '/subscriptions/123/providers/Microsoft.Authorization/roleDefinitions/acdd72a7' |
| 42 | + mock_client.list.return_value = [mock_role_def] |
25 | 43 |
|
26 | | - # action (using a full id) |
27 | | - test_full_id = '/subscriptions/0b1f6471-1bf0-4dda-aec3-cb9272123456/providers/microsoft.authorization/roleDefinitions/5370bbf4-6b73-4417-969b-8f2e6e123456' |
28 | | - self.assertEqual(test_full_id, _resolve_role_id(test_full_id, 'foobar', mock_client)) |
| 44 | + result = _resolve_role_id('Reader', '/subscriptions/123', mock_client) |
| 45 | + |
| 46 | + assert result == mock_role_def.id |
| 47 | + mock_client.list.assert_called_once_with('/subscriptions/123', "roleName eq 'Reader'") |
| 48 | + |
| 49 | + @pytest.mark.parametrize("api_response,error_contains", [ |
| 50 | + ([], "doesn't exist"), # Not found |
| 51 | + ([mock.Mock(id='id1'), mock.Mock(id='id2')], "More than one role"), # Multiple matches |
| 52 | + ]) |
| 53 | + def test_role_name_error_cases(self, mock_client, api_response, error_contains): |
| 54 | + """Role name lookup raises CLIError for not found or multiple matches.""" |
| 55 | + from knack.util import CLIError |
| 56 | + mock_client.list.return_value = api_response |
| 57 | + |
| 58 | + with pytest.raises(CLIError, match=error_contains): |
| 59 | + _resolve_role_id('SomeRole', '/subscriptions/123', mock_client) |
| 60 | + |
| 61 | + |
| 62 | +class TestSearchRoleAssignments: |
| 63 | + """Tests for _search_role_assignments function, focusing on role filtering.""" |
| 64 | + |
| 65 | + @pytest.fixture |
| 66 | + def mock_clients(self): |
| 67 | + assignments_client = mock.Mock() |
| 68 | + definitions_client = mock.Mock() |
| 69 | + definitions_client._config.subscription_id = '00000000-0000-0000-0000-000000000000' |
| 70 | + return assignments_client, definitions_client |
| 71 | + |
| 72 | + @staticmethod |
| 73 | + def _create_assignment(scope, role_definition_id, principal_id='principal-1'): |
| 74 | + assignment = mock.Mock() |
| 75 | + assignment.scope = scope |
| 76 | + assignment.role_definition_id = role_definition_id |
| 77 | + assignment.principal_id = principal_id |
| 78 | + return assignment |
| 79 | + |
| 80 | + @pytest.mark.parametrize("scope,role_def_format", [ |
| 81 | + # Root scope with tenant-format role definition ID |
| 82 | + ('/', '/providers/Microsoft.Authorization/roleDefinitions/{guid}'), |
| 83 | + # Management group scope with tenant-format role definition ID |
| 84 | + ('/providers/Microsoft.Management/managementGroups/my-mg', |
| 85 | + '/providers/Microsoft.Authorization/roleDefinitions/{guid}'), |
| 86 | + # Subscription scope with subscription-format role definition ID |
| 87 | + ('/subscriptions/00000000-0000-0000-0000-000000000000', |
| 88 | + '/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/{guid}'), |
| 89 | + ]) |
| 90 | + def test_guid_filter_matches_across_scopes(self, mock_clients, scope, role_def_format): |
| 91 | + """GUID filter matches assignments at various scopes with different roleDefinitionId formats.""" |
| 92 | + assignments_client, definitions_client = mock_clients |
| 93 | + role_guid = 'acdd72a7-3385-48ef-bd42-f606fba81ae7' |
| 94 | + role_def_id = role_def_format.format(guid=role_guid) |
| 95 | + |
| 96 | + assignments_client.list_for_scope.return_value = [ |
| 97 | + self._create_assignment(scope, role_def_id), |
| 98 | + ] |
| 99 | + |
| 100 | + result = _search_role_assignments( |
| 101 | + assignments_client, definitions_client, |
| 102 | + scope=scope, assignee_object_id=None, role=role_guid, |
| 103 | + include_inherited=False, include_groups=False |
| 104 | + ) |
| 105 | + |
| 106 | + assert len(result) == 1 |
| 107 | + |
| 108 | + def test_different_role_guid_does_not_match(self, mock_clients): |
| 109 | + """Assignments with different role GUIDs are filtered out.""" |
| 110 | + assignments_client, definitions_client = mock_clients |
| 111 | + filter_guid = 'acdd72a7-3385-48ef-bd42-f606fba81ae7' |
| 112 | + other_guid = 'b24988ac-6180-42a0-ab88-20f7382dd24c' |
| 113 | + |
| 114 | + assignments_client.list_for_scope.return_value = [ |
| 115 | + self._create_assignment('/', f'/providers/Microsoft.Authorization/roleDefinitions/{other_guid}'), |
| 116 | + ] |
| 117 | + |
| 118 | + result = _search_role_assignments( |
| 119 | + assignments_client, definitions_client, |
| 120 | + scope='/', assignee_object_id=None, role=filter_guid, |
| 121 | + include_inherited=False, include_groups=False |
| 122 | + ) |
| 123 | + |
| 124 | + assert len(result) == 0 |
| 125 | + |
| 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 |
0 commit comments