cumulusci-plus 5.0.21__py3-none-any.whl → 5.0.43__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cumulusci/__about__.py +1 -1
- cumulusci/cli/logger.py +2 -2
- cumulusci/cli/service.py +20 -0
- cumulusci/cli/task.py +19 -3
- cumulusci/cli/tests/test_error.py +3 -1
- cumulusci/cli/tests/test_flow.py +279 -2
- cumulusci/cli/tests/test_org.py +5 -0
- cumulusci/cli/tests/test_service.py +15 -12
- cumulusci/cli/tests/test_task.py +122 -2
- cumulusci/cli/tests/utils.py +1 -4
- cumulusci/core/config/__init__.py +1 -0
- cumulusci/core/config/base_task_flow_config.py +26 -1
- cumulusci/core/config/org_config.py +2 -1
- cumulusci/core/config/project_config.py +14 -20
- cumulusci/core/config/scratch_org_config.py +12 -0
- cumulusci/core/config/tests/test_config.py +1 -0
- cumulusci/core/config/tests/test_config_expensive.py +9 -3
- cumulusci/core/config/universal_config.py +3 -4
- cumulusci/core/dependencies/base.py +5 -1
- cumulusci/core/dependencies/dependencies.py +1 -1
- cumulusci/core/dependencies/github.py +1 -2
- cumulusci/core/dependencies/resolvers.py +1 -1
- cumulusci/core/dependencies/tests/test_dependencies.py +1 -1
- cumulusci/core/dependencies/tests/test_resolvers.py +1 -1
- cumulusci/core/flowrunner.py +90 -6
- cumulusci/core/github.py +1 -1
- cumulusci/core/sfdx.py +3 -1
- cumulusci/core/source_transforms/tests/test_transforms.py +1 -1
- cumulusci/core/source_transforms/transforms.py +1 -1
- cumulusci/core/tasks.py +13 -2
- cumulusci/core/tests/test_flowrunner.py +100 -0
- cumulusci/core/tests/test_tasks.py +65 -0
- cumulusci/core/utils.py +3 -1
- cumulusci/core/versions.py +1 -1
- cumulusci/cumulusci.yml +73 -1
- cumulusci/oauth/client.py +1 -1
- cumulusci/plugins/plugin_base.py +5 -3
- cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
- cumulusci/salesforce_api/rest_deploy.py +1 -1
- cumulusci/schema/cumulusci.jsonschema.json +69 -0
- cumulusci/tasks/apex/anon.py +1 -1
- cumulusci/tasks/apex/testrunner.py +421 -144
- cumulusci/tasks/apex/tests/test_apex_tasks.py +917 -1
- cumulusci/tasks/bulkdata/extract.py +0 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +1 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +1 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +1 -1
- cumulusci/tasks/bulkdata/generate_and_load_data.py +136 -12
- cumulusci/tasks/bulkdata/mapping_parser.py +139 -44
- cumulusci/tasks/bulkdata/select_utils.py +1 -1
- cumulusci/tasks/bulkdata/snowfakery.py +100 -25
- cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +159 -0
- cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
- cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +763 -1
- cumulusci/tasks/bulkdata/tests/test_select_utils.py +46 -0
- cumulusci/tasks/bulkdata/tests/test_snowfakery.py +133 -0
- cumulusci/tasks/create_package_version.py +190 -16
- cumulusci/tasks/datadictionary.py +1 -1
- cumulusci/tasks/metadata_etl/__init__.py +2 -0
- cumulusci/tasks/metadata_etl/applications.py +256 -0
- cumulusci/tasks/metadata_etl/base.py +7 -3
- cumulusci/tasks/metadata_etl/layouts.py +1 -1
- cumulusci/tasks/metadata_etl/permissions.py +1 -1
- cumulusci/tasks/metadata_etl/remote_site_settings.py +2 -2
- cumulusci/tasks/metadata_etl/tests/test_applications.py +710 -0
- cumulusci/tasks/push/README.md +15 -17
- cumulusci/tasks/release_notes/README.md +13 -13
- cumulusci/tasks/release_notes/generator.py +13 -8
- cumulusci/tasks/robotframework/tests/test_robotframework.py +6 -1
- cumulusci/tasks/salesforce/Deploy.py +53 -2
- cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
- cumulusci/tasks/salesforce/__init__.py +1 -0
- cumulusci/tasks/salesforce/assign_ps_psg.py +448 -0
- cumulusci/tasks/salesforce/composite.py +1 -1
- cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
- cumulusci/tasks/salesforce/enable_prediction.py +5 -1
- cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
- cumulusci/tasks/salesforce/insert_record.py +18 -19
- cumulusci/tasks/salesforce/sourcetracking.py +1 -1
- cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
- cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
- cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
- cumulusci/tasks/salesforce/tests/test_enable_prediction.py +4 -2
- cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
- cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
- cumulusci/tasks/salesforce/tests/test_update_external_auth_identity_provider.py +927 -0
- cumulusci/tasks/salesforce/tests/test_update_external_credential.py +1427 -0
- cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
- cumulusci/tasks/salesforce/tests/test_update_record.py +512 -0
- cumulusci/tasks/salesforce/update_dependencies.py +2 -2
- cumulusci/tasks/salesforce/update_external_auth_identity_provider.py +551 -0
- cumulusci/tasks/salesforce/update_external_credential.py +647 -0
- cumulusci/tasks/salesforce/update_named_credential.py +441 -0
- cumulusci/tasks/salesforce/update_profile.py +17 -13
- cumulusci/tasks/salesforce/update_record.py +217 -0
- cumulusci/tasks/salesforce/users/permsets.py +62 -5
- cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
- cumulusci/tasks/sfdmu/__init__.py +0 -0
- cumulusci/tasks/sfdmu/sfdmu.py +376 -0
- cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
- cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
- cumulusci/tasks/sfdmu/tests/test_sfdmu.py +1012 -0
- cumulusci/tasks/tests/test_create_package_version.py +716 -1
- cumulusci/tasks/tests/test_util.py +42 -0
- cumulusci/tasks/util.py +37 -1
- cumulusci/tasks/utility/copyContents.py +402 -0
- cumulusci/tasks/utility/credentialManager.py +302 -0
- cumulusci/tasks/utility/directoryRecreator.py +30 -0
- cumulusci/tasks/utility/env_management.py +1 -1
- cumulusci/tasks/utility/secretsToEnv.py +135 -0
- cumulusci/tasks/utility/tests/test_copyContents.py +1719 -0
- cumulusci/tasks/utility/tests/test_credentialManager.py +1150 -0
- cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
- cumulusci/tasks/utility/tests/test_secretsToEnv.py +1118 -0
- cumulusci/tests/test_integration_infrastructure.py +3 -1
- cumulusci/tests/test_utils.py +70 -6
- cumulusci/utils/__init__.py +54 -9
- cumulusci/utils/classutils.py +5 -2
- cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
- cumulusci/utils/options.py +23 -1
- cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
- cumulusci/utils/yaml/cumulusci_yml.py +8 -3
- cumulusci/utils/yaml/model_parser.py +2 -2
- cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
- cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
- cumulusci/vcs/base.py +23 -15
- cumulusci/vcs/bootstrap.py +5 -4
- cumulusci/vcs/utils/list_modified_files.py +189 -0
- cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/METADATA +11 -10
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/RECORD +135 -104
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/WHEEL +1 -1
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from unittest import mock
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
import responses
|
|
6
|
+
|
|
7
|
+
from cumulusci.core.exceptions import SalesforceException, TaskOptionsError
|
|
8
|
+
from cumulusci.tasks.salesforce.assign_ps_psg import (
|
|
9
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
10
|
+
PermissionSetGroupAssignmentsOption,
|
|
11
|
+
build_name_conditions,
|
|
12
|
+
)
|
|
13
|
+
from cumulusci.tests.util import CURRENT_SF_API_VERSION
|
|
14
|
+
|
|
15
|
+
from .util import create_task
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestPermissionSetGroupAssignmentsOption:
|
|
19
|
+
"""Test PermissionSetGroupAssignmentsOption validation"""
|
|
20
|
+
|
|
21
|
+
def test_validate_dict_input(self):
|
|
22
|
+
"""Test validation with dict input"""
|
|
23
|
+
assignments = {"PSG1": ["PS1", "PS2"], "PSG2": ["PS3"]}
|
|
24
|
+
result = PermissionSetGroupAssignmentsOption.validate(assignments)
|
|
25
|
+
assert result == {"PSG1": ["PS1", "PS2"], "PSG2": ["PS3"]}
|
|
26
|
+
|
|
27
|
+
def test_validate_dict_with_single_value(self):
|
|
28
|
+
"""Test validation with dict where value is a single string"""
|
|
29
|
+
assignments = {"PSG1": "PS1", "PSG2": ["PS2", "PS3"]}
|
|
30
|
+
result = PermissionSetGroupAssignmentsOption.validate(assignments)
|
|
31
|
+
assert result == {"PSG1": ["PS1"], "PSG2": ["PS2", "PS3"]}
|
|
32
|
+
|
|
33
|
+
def test_validate_json_string(self):
|
|
34
|
+
"""Test validation with JSON string"""
|
|
35
|
+
json_str = '{"PSG1": ["PS1", "PS2"], "PSG2": ["PS3"]}'
|
|
36
|
+
result = PermissionSetGroupAssignmentsOption.validate(json_str)
|
|
37
|
+
assert result == {"PSG1": ["PS1", "PS2"], "PSG2": ["PS3"]}
|
|
38
|
+
|
|
39
|
+
def test_validate_json_string_with_single_value(self):
|
|
40
|
+
"""Test validation with JSON string where value is a single string"""
|
|
41
|
+
json_str = '{"PSG1": "PS1", "PSG2": ["PS2"]}'
|
|
42
|
+
result = PermissionSetGroupAssignmentsOption.validate(json_str)
|
|
43
|
+
assert result == {"PSG1": ["PS1"], "PSG2": ["PS2"]}
|
|
44
|
+
|
|
45
|
+
def test_validate_command_line_format(self):
|
|
46
|
+
"""Test validation with command line format"""
|
|
47
|
+
cmd_str = "PSG1:PS1,PS2;PSG2:PS3,PS4"
|
|
48
|
+
result = PermissionSetGroupAssignmentsOption.validate(cmd_str)
|
|
49
|
+
assert result == {"PSG1": ["PS1", "PS2"], "PSG2": ["PS3", "PS4"]}
|
|
50
|
+
|
|
51
|
+
def test_validate_command_line_format_with_spaces(self):
|
|
52
|
+
"""Test validation with command line format with spaces"""
|
|
53
|
+
cmd_str = "PSG1: PS1 , PS2 ; PSG2: PS3 , PS4"
|
|
54
|
+
result = PermissionSetGroupAssignmentsOption.validate(cmd_str)
|
|
55
|
+
assert result == {"PSG1": ["PS1", "PS2"], "PSG2": ["PS3", "PS4"]}
|
|
56
|
+
|
|
57
|
+
def test_validate_invalid_type(self):
|
|
58
|
+
"""Test validation with invalid type"""
|
|
59
|
+
with pytest.raises(TaskOptionsError, match="Invalid format"):
|
|
60
|
+
PermissionSetGroupAssignmentsOption.validate(123)
|
|
61
|
+
|
|
62
|
+
def test_validate_invalid_json_string(self):
|
|
63
|
+
"""Test validation with invalid JSON string"""
|
|
64
|
+
import json
|
|
65
|
+
|
|
66
|
+
with pytest.raises((TaskOptionsError, json.JSONDecodeError)):
|
|
67
|
+
PermissionSetGroupAssignmentsOption.validate('{"invalid": json}')
|
|
68
|
+
|
|
69
|
+
def test_validate_json_array_instead_of_dict(self):
|
|
70
|
+
"""Test validation with JSON array instead of dict"""
|
|
71
|
+
with pytest.raises(TaskOptionsError, match="Expected dict"):
|
|
72
|
+
PermissionSetGroupAssignmentsOption.validate('["PSG1", "PSG2"]')
|
|
73
|
+
|
|
74
|
+
def test_from_str_valid_format(self):
|
|
75
|
+
"""Test from_str with valid format"""
|
|
76
|
+
cmd_str = "PSG1:PS1,PS2;PSG2:PS3"
|
|
77
|
+
result = PermissionSetGroupAssignmentsOption.from_str(cmd_str)
|
|
78
|
+
assert result == {"PSG1": ["PS1", "PS2"], "PSG2": ["PS3"]}
|
|
79
|
+
|
|
80
|
+
def test_from_str_invalid_format(self):
|
|
81
|
+
"""Test from_str with invalid format"""
|
|
82
|
+
with pytest.raises(TaskOptionsError, match="Invalid format"):
|
|
83
|
+
PermissionSetGroupAssignmentsOption.from_str("invalid")
|
|
84
|
+
|
|
85
|
+
def test_from_str_empty_string(self):
|
|
86
|
+
"""Test from_str with empty string"""
|
|
87
|
+
with pytest.raises(TaskOptionsError, match="Invalid format"):
|
|
88
|
+
PermissionSetGroupAssignmentsOption.from_str("")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TestAssignPermissionSetToPermissionSetGroup:
|
|
92
|
+
"""Test AssignPermissionSetToPermissionSetGroup task"""
|
|
93
|
+
|
|
94
|
+
def test_init_options_with_namespace(self):
|
|
95
|
+
"""Test _init_options with namespace provided"""
|
|
96
|
+
task = create_task(
|
|
97
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
98
|
+
{
|
|
99
|
+
"assignments": {"PSG1": ["PS1"]},
|
|
100
|
+
"namespace_inject": "test_namespace",
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
assert task.parsed_options.namespace_inject == "test_namespace"
|
|
104
|
+
|
|
105
|
+
def test_init_options_without_namespace(self):
|
|
106
|
+
"""Test _init_options without namespace (uses project config)"""
|
|
107
|
+
task = create_task(
|
|
108
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
109
|
+
{"assignments": {"PSG1": ["PS1"]}},
|
|
110
|
+
)
|
|
111
|
+
task.project_config.project__package__namespace = "project_namespace"
|
|
112
|
+
task._init_options({})
|
|
113
|
+
assert task.parsed_options.namespace_inject == "project_namespace"
|
|
114
|
+
|
|
115
|
+
def test_init_options_with_managed(self):
|
|
116
|
+
"""Test _init_options with managed flag"""
|
|
117
|
+
task = create_task(
|
|
118
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
119
|
+
{
|
|
120
|
+
"assignments": {"PSG1": ["PS1"]},
|
|
121
|
+
"managed": True,
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
assert task.parsed_options.managed is True
|
|
125
|
+
|
|
126
|
+
def test_init_options_without_managed(self):
|
|
127
|
+
"""Test _init_options without managed flag (determines from config)"""
|
|
128
|
+
with mock.patch(
|
|
129
|
+
"cumulusci.tasks.salesforce.assign_ps_psg.determine_managed_mode",
|
|
130
|
+
return_value=False,
|
|
131
|
+
):
|
|
132
|
+
task = create_task(
|
|
133
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
134
|
+
{"assignments": {"PSG1": ["PS1"]}},
|
|
135
|
+
)
|
|
136
|
+
task._init_options({})
|
|
137
|
+
assert task.parsed_options.managed is False
|
|
138
|
+
|
|
139
|
+
def test_init_options_namespaced_org(self):
|
|
140
|
+
"""Test _init_options sets namespaced_org correctly"""
|
|
141
|
+
task = create_task(
|
|
142
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
143
|
+
{
|
|
144
|
+
"assignments": {"PSG1": ["PS1"]},
|
|
145
|
+
"namespace_inject": "test_namespace",
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
task.org_config.namespace = "test_namespace"
|
|
149
|
+
task._init_options({})
|
|
150
|
+
assert task.namespaced_org is True
|
|
151
|
+
|
|
152
|
+
def test_init_options_non_namespaced_org(self):
|
|
153
|
+
"""Test _init_options sets namespaced_org to False when namespaces don't match"""
|
|
154
|
+
task = create_task(
|
|
155
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
156
|
+
{
|
|
157
|
+
"assignments": {"PSG1": ["PS1"]},
|
|
158
|
+
"namespace_inject": "test_namespace",
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
task.org_config.namespace = "different_namespace"
|
|
162
|
+
task._init_options({})
|
|
163
|
+
assert task.namespaced_org is False
|
|
164
|
+
|
|
165
|
+
@responses.activate
|
|
166
|
+
def test_run_task_empty_assignments(self):
|
|
167
|
+
"""Test _run_task with empty assignments"""
|
|
168
|
+
task = create_task(
|
|
169
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
170
|
+
{"assignments": {}},
|
|
171
|
+
)
|
|
172
|
+
task._run_task()
|
|
173
|
+
# Should not raise and should not make any API calls
|
|
174
|
+
assert len(responses.calls) == 0
|
|
175
|
+
|
|
176
|
+
@responses.activate
|
|
177
|
+
def test_run_task_success(self):
|
|
178
|
+
"""Test _run_task with successful assignment"""
|
|
179
|
+
task = create_task(
|
|
180
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
181
|
+
{"assignments": {"PSG1": ["PS1", "PS2"]}},
|
|
182
|
+
)
|
|
183
|
+
task._init_task()
|
|
184
|
+
|
|
185
|
+
# Mock PSG query
|
|
186
|
+
responses.add(
|
|
187
|
+
method="GET",
|
|
188
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id%2C+DeveloperName%2C+NamespacePrefix+FROM+PermissionSetGroup+WHERE+%28DeveloperName+%3D+%27PSG1%27%29",
|
|
189
|
+
status=200,
|
|
190
|
+
json={
|
|
191
|
+
"totalSize": 1,
|
|
192
|
+
"done": True,
|
|
193
|
+
"records": [
|
|
194
|
+
{
|
|
195
|
+
"Id": "0PG000000000001",
|
|
196
|
+
"DeveloperName": "PSG1",
|
|
197
|
+
"NamespacePrefix": None,
|
|
198
|
+
}
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Mock PS query
|
|
204
|
+
responses.add(
|
|
205
|
+
method="GET",
|
|
206
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id%2C+Name%2C+NamespacePrefix+FROM+PermissionSet+WHERE+IsOwnedByProfile+%3D+false+AND+%28Name+%3D+%27PS1%27+OR+Name+%3D+%27PS2%27%29",
|
|
207
|
+
status=200,
|
|
208
|
+
json={
|
|
209
|
+
"totalSize": 2,
|
|
210
|
+
"done": True,
|
|
211
|
+
"records": [
|
|
212
|
+
{"Id": "0PS000000000001", "Name": "PS1", "NamespacePrefix": None},
|
|
213
|
+
{"Id": "0PS000000000002", "Name": "PS2", "NamespacePrefix": None},
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Mock Composite API
|
|
219
|
+
responses.add(
|
|
220
|
+
method="POST",
|
|
221
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
|
|
222
|
+
status=200,
|
|
223
|
+
json=[
|
|
224
|
+
{"id": "0PGC00000000001", "success": True, "errors": []},
|
|
225
|
+
{"id": "0PGC00000000002", "success": True, "errors": []},
|
|
226
|
+
],
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
task._run_task()
|
|
230
|
+
|
|
231
|
+
assert len(responses.calls) == 3
|
|
232
|
+
composite_request = json.loads(responses.calls[2].request.body)
|
|
233
|
+
assert len(composite_request["records"]) == 2
|
|
234
|
+
assert (
|
|
235
|
+
composite_request["records"][0]["PermissionSetGroupId"] == "0PG000000000001"
|
|
236
|
+
)
|
|
237
|
+
assert composite_request["records"][0]["PermissionSetId"] == "0PS000000000001"
|
|
238
|
+
|
|
239
|
+
@responses.activate
|
|
240
|
+
def test_run_task_missing_psg(self):
|
|
241
|
+
"""Test _run_task with missing Permission Set Group"""
|
|
242
|
+
task = create_task(
|
|
243
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
244
|
+
{"assignments": {"MissingPSG": ["PS1"]}, "fail_on_error": False},
|
|
245
|
+
)
|
|
246
|
+
task._init_task()
|
|
247
|
+
|
|
248
|
+
# Mock PSG query - no results
|
|
249
|
+
responses.add(
|
|
250
|
+
method="GET",
|
|
251
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id%2C+DeveloperName%2C+NamespacePrefix+FROM+PermissionSetGroup+WHERE+%28DeveloperName+%3D+%27MissingPSG%27%29",
|
|
252
|
+
status=200,
|
|
253
|
+
json={"totalSize": 0, "done": True, "records": []},
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Mock PS query
|
|
257
|
+
responses.add(
|
|
258
|
+
method="GET",
|
|
259
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id%2C+Name%2C+NamespacePrefix+FROM+PermissionSet+WHERE+IsOwnedByProfile+%3D+false+AND+%28Name+%3D+%27PS1%27%29",
|
|
260
|
+
status=200,
|
|
261
|
+
json={
|
|
262
|
+
"totalSize": 1,
|
|
263
|
+
"done": True,
|
|
264
|
+
"records": [
|
|
265
|
+
{"Id": "0PS000000000001", "Name": "PS1", "NamespacePrefix": None}
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
task._run_task()
|
|
271
|
+
|
|
272
|
+
# Should not create any records since PSG is missing
|
|
273
|
+
assert len(responses.calls) == 2
|
|
274
|
+
# No composite API call should be made
|
|
275
|
+
|
|
276
|
+
@responses.activate
|
|
277
|
+
def test_run_task_missing_ps(self):
|
|
278
|
+
"""Test _run_task with missing Permission Set"""
|
|
279
|
+
task = create_task(
|
|
280
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
281
|
+
{"assignments": {"PSG1": ["MissingPS"]}, "fail_on_error": False},
|
|
282
|
+
)
|
|
283
|
+
task._init_task()
|
|
284
|
+
|
|
285
|
+
# Mock PSG query
|
|
286
|
+
responses.add(
|
|
287
|
+
method="GET",
|
|
288
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id%2C+DeveloperName%2C+NamespacePrefix+FROM+PermissionSetGroup+WHERE+%28DeveloperName+%3D+%27PSG1%27%29",
|
|
289
|
+
status=200,
|
|
290
|
+
json={
|
|
291
|
+
"totalSize": 1,
|
|
292
|
+
"done": True,
|
|
293
|
+
"records": [
|
|
294
|
+
{
|
|
295
|
+
"Id": "0PG000000000001",
|
|
296
|
+
"DeveloperName": "PSG1",
|
|
297
|
+
"NamespacePrefix": None,
|
|
298
|
+
}
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Mock PS query - no results
|
|
304
|
+
responses.add(
|
|
305
|
+
method="GET",
|
|
306
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id%2C+Name%2C+NamespacePrefix+FROM+PermissionSet+WHERE+IsOwnedByProfile+%3D+false+AND+%28Name+%3D+%27MissingPS%27%29",
|
|
307
|
+
status=200,
|
|
308
|
+
json={"totalSize": 0, "done": True, "records": []},
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
task._run_task()
|
|
312
|
+
|
|
313
|
+
# Should not create any records since PS is missing
|
|
314
|
+
assert len(responses.calls) == 2
|
|
315
|
+
# No composite API call should be made
|
|
316
|
+
|
|
317
|
+
@responses.activate
|
|
318
|
+
def test_run_task_batch_processing(self):
|
|
319
|
+
"""Test _run_task processes records in batches of 200"""
|
|
320
|
+
# Create 250 assignments to test batching
|
|
321
|
+
assignments = {}
|
|
322
|
+
for i in range(250):
|
|
323
|
+
psg_name = f"PSG{i // 10}"
|
|
324
|
+
ps_name = f"PS{i}"
|
|
325
|
+
if psg_name not in assignments:
|
|
326
|
+
assignments[psg_name] = []
|
|
327
|
+
assignments[psg_name].append(ps_name)
|
|
328
|
+
|
|
329
|
+
task = create_task(
|
|
330
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
331
|
+
{"assignments": assignments},
|
|
332
|
+
)
|
|
333
|
+
task._init_task()
|
|
334
|
+
|
|
335
|
+
# Mock PSG query
|
|
336
|
+
psg_records = [
|
|
337
|
+
{"Id": f"0PG{i:012d}", "DeveloperName": f"PSG{i}", "NamespacePrefix": None}
|
|
338
|
+
for i in range(25)
|
|
339
|
+
]
|
|
340
|
+
responses.add(
|
|
341
|
+
method="GET",
|
|
342
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
343
|
+
status=200,
|
|
344
|
+
json={"totalSize": 25, "done": True, "records": psg_records},
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Mock PS query
|
|
348
|
+
ps_records = [
|
|
349
|
+
{"Id": f"0PS{i:012d}", "Name": f"PS{i}", "NamespacePrefix": None}
|
|
350
|
+
for i in range(250)
|
|
351
|
+
]
|
|
352
|
+
responses.add(
|
|
353
|
+
method="GET",
|
|
354
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
355
|
+
status=200,
|
|
356
|
+
json={"totalSize": 250, "done": True, "records": ps_records},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Mock Composite API calls (2 batches: 200 + 50)
|
|
360
|
+
responses.add(
|
|
361
|
+
method="POST",
|
|
362
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
|
|
363
|
+
status=200,
|
|
364
|
+
json=[
|
|
365
|
+
{"id": f"0PGC{i:011d}", "success": True, "errors": []}
|
|
366
|
+
for i in range(200)
|
|
367
|
+
],
|
|
368
|
+
)
|
|
369
|
+
responses.add(
|
|
370
|
+
method="POST",
|
|
371
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
|
|
372
|
+
status=200,
|
|
373
|
+
json=[
|
|
374
|
+
{"id": f"0PGC{i:011d}", "success": True, "errors": []}
|
|
375
|
+
for i in range(200, 250)
|
|
376
|
+
],
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
task._run_task()
|
|
380
|
+
|
|
381
|
+
# Should have 2 composite API calls
|
|
382
|
+
composite_calls = [
|
|
383
|
+
call for call in responses.calls if "composite/sobjects" in call.request.url
|
|
384
|
+
]
|
|
385
|
+
assert len(composite_calls) == 2
|
|
386
|
+
assert len(json.loads(composite_calls[0].request.body)["records"]) == 200
|
|
387
|
+
assert len(json.loads(composite_calls[1].request.body)["records"]) == 50
|
|
388
|
+
|
|
389
|
+
def test_get_permission_set_group_ids_empty_list(self):
|
|
390
|
+
"""Test _get_permission_set_group_ids with empty list"""
|
|
391
|
+
task = create_task(
|
|
392
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
393
|
+
{"assignments": {"PSG1": ["PS1"]}},
|
|
394
|
+
)
|
|
395
|
+
task._init_options({})
|
|
396
|
+
task._get_permission_set_group_ids([])
|
|
397
|
+
assert task.psg_ids == {}
|
|
398
|
+
|
|
399
|
+
@responses.activate
|
|
400
|
+
def test_get_permission_set_group_ids_success(self):
|
|
401
|
+
"""Test _get_permission_set_group_ids with successful query"""
|
|
402
|
+
task = create_task(
|
|
403
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
404
|
+
{"assignments": {"PSG1": ["PS1"]}},
|
|
405
|
+
)
|
|
406
|
+
task._init_options({})
|
|
407
|
+
task._init_task()
|
|
408
|
+
|
|
409
|
+
responses.add(
|
|
410
|
+
method="GET",
|
|
411
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
412
|
+
status=200,
|
|
413
|
+
json={
|
|
414
|
+
"totalSize": 1,
|
|
415
|
+
"done": True,
|
|
416
|
+
"records": [
|
|
417
|
+
{
|
|
418
|
+
"Id": "0PG000000000001",
|
|
419
|
+
"DeveloperName": "PSG1",
|
|
420
|
+
"NamespacePrefix": None,
|
|
421
|
+
}
|
|
422
|
+
],
|
|
423
|
+
},
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
task._get_permission_set_group_ids(["PSG1"])
|
|
427
|
+
assert task.psg_ids["PSG1"] == "0PG000000000001"
|
|
428
|
+
|
|
429
|
+
@responses.activate
|
|
430
|
+
def test_get_permission_set_group_ids_with_namespace(self):
|
|
431
|
+
"""Test _get_permission_set_group_ids with namespace"""
|
|
432
|
+
task = create_task(
|
|
433
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
434
|
+
{"assignments": {"NS__PSG1": ["PS1"]}, "namespace_inject": "NS"},
|
|
435
|
+
)
|
|
436
|
+
task._init_options({})
|
|
437
|
+
task._init_task()
|
|
438
|
+
|
|
439
|
+
responses.add(
|
|
440
|
+
method="GET",
|
|
441
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
442
|
+
status=200,
|
|
443
|
+
json={
|
|
444
|
+
"totalSize": 1,
|
|
445
|
+
"done": True,
|
|
446
|
+
"records": [
|
|
447
|
+
{
|
|
448
|
+
"Id": "0PG000000000001",
|
|
449
|
+
"DeveloperName": "PSG1",
|
|
450
|
+
"NamespacePrefix": "NS",
|
|
451
|
+
}
|
|
452
|
+
],
|
|
453
|
+
},
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
task._get_permission_set_group_ids(["NS__PSG1"])
|
|
457
|
+
assert "NS__PSG1" in task.psg_ids
|
|
458
|
+
|
|
459
|
+
@responses.activate
|
|
460
|
+
def test_get_permission_set_group_ids_query_error(self):
|
|
461
|
+
"""Test _get_permission_set_group_ids with query error"""
|
|
462
|
+
task = create_task(
|
|
463
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
464
|
+
{"assignments": {"PSG1": ["PS1"]}},
|
|
465
|
+
)
|
|
466
|
+
task._init_options({})
|
|
467
|
+
task._init_task()
|
|
468
|
+
|
|
469
|
+
responses.add(
|
|
470
|
+
method="GET",
|
|
471
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
472
|
+
status=400,
|
|
473
|
+
json=[{"errorCode": "INVALID_FIELD", "message": "Invalid field"}],
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
with pytest.raises(
|
|
477
|
+
SalesforceException, match="Error querying Permission Set Groups"
|
|
478
|
+
):
|
|
479
|
+
task()
|
|
480
|
+
|
|
481
|
+
def test_get_permission_set_ids_empty_list(self):
|
|
482
|
+
"""Test _get_permission_set_ids with empty list"""
|
|
483
|
+
task = create_task(
|
|
484
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
485
|
+
{"assignments": {"PSG1": ["PS1"]}},
|
|
486
|
+
)
|
|
487
|
+
task._init_options({})
|
|
488
|
+
task._get_permission_set_ids([])
|
|
489
|
+
assert task.ps_ids == {}
|
|
490
|
+
|
|
491
|
+
@responses.activate
|
|
492
|
+
def test_get_permission_set_ids_success(self):
|
|
493
|
+
"""Test _get_permission_set_ids with successful query"""
|
|
494
|
+
task = create_task(
|
|
495
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
496
|
+
{"assignments": {"PSG1": ["PS1", "PS2"]}},
|
|
497
|
+
)
|
|
498
|
+
task._init_options({})
|
|
499
|
+
task._init_task()
|
|
500
|
+
|
|
501
|
+
responses.add(
|
|
502
|
+
method="GET",
|
|
503
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
504
|
+
status=200,
|
|
505
|
+
json={
|
|
506
|
+
"totalSize": 2,
|
|
507
|
+
"done": True,
|
|
508
|
+
"records": [
|
|
509
|
+
{"Id": "0PS000000000001", "Name": "PS1", "NamespacePrefix": None},
|
|
510
|
+
{"Id": "0PS000000000002", "Name": "PS2", "NamespacePrefix": None},
|
|
511
|
+
],
|
|
512
|
+
},
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
task._get_permission_set_ids(["PS1", "PS2"])
|
|
516
|
+
assert task.ps_ids["PS1"] == "0PS000000000001"
|
|
517
|
+
assert task.ps_ids["PS2"] == "0PS000000000002"
|
|
518
|
+
|
|
519
|
+
@responses.activate
|
|
520
|
+
def test_get_permission_set_ids_removes_duplicates(self):
|
|
521
|
+
"""Test _get_permission_set_ids removes duplicates"""
|
|
522
|
+
task = create_task(
|
|
523
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
524
|
+
{"assignments": {"PSG1": ["PS1"]}},
|
|
525
|
+
)
|
|
526
|
+
task._init_options({})
|
|
527
|
+
task._init_task()
|
|
528
|
+
|
|
529
|
+
responses.add(
|
|
530
|
+
method="GET",
|
|
531
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
532
|
+
status=200,
|
|
533
|
+
json={
|
|
534
|
+
"totalSize": 1,
|
|
535
|
+
"done": True,
|
|
536
|
+
"records": [
|
|
537
|
+
{"Id": "0PS000000000001", "Name": "PS1", "NamespacePrefix": None}
|
|
538
|
+
],
|
|
539
|
+
},
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
task._get_permission_set_ids(["PS1", "PS1", "PS1"])
|
|
543
|
+
assert len(task.ps_ids) == 1
|
|
544
|
+
assert task.ps_ids["PS1"] == "0PS000000000001"
|
|
545
|
+
|
|
546
|
+
@responses.activate
|
|
547
|
+
def test_get_permission_set_ids_with_namespace(self):
|
|
548
|
+
"""Test _get_permission_set_ids with namespace"""
|
|
549
|
+
task = create_task(
|
|
550
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
551
|
+
{"assignments": {"PSG1": ["NS__PS1"]}, "namespace_inject": "NS"},
|
|
552
|
+
)
|
|
553
|
+
task._init_options({})
|
|
554
|
+
task._init_task()
|
|
555
|
+
|
|
556
|
+
responses.add(
|
|
557
|
+
method="GET",
|
|
558
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
559
|
+
status=200,
|
|
560
|
+
json={
|
|
561
|
+
"totalSize": 1,
|
|
562
|
+
"done": True,
|
|
563
|
+
"records": [
|
|
564
|
+
{"Id": "0PS000000000001", "Name": "PS1", "NamespacePrefix": "NS"}
|
|
565
|
+
],
|
|
566
|
+
},
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
task._get_permission_set_ids(["NS__PS1"])
|
|
570
|
+
assert "NS__PS1" in task.ps_ids
|
|
571
|
+
|
|
572
|
+
@responses.activate
|
|
573
|
+
def test_get_permission_set_ids_query_error(self):
|
|
574
|
+
"""Test _get_permission_set_ids with query error"""
|
|
575
|
+
task = create_task(
|
|
576
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
577
|
+
{"assignments": {"PSG1": ["PS1"]}},
|
|
578
|
+
)
|
|
579
|
+
task._init_options({})
|
|
580
|
+
task._init_task()
|
|
581
|
+
|
|
582
|
+
responses.add(
|
|
583
|
+
method="GET",
|
|
584
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
585
|
+
status=200,
|
|
586
|
+
json={
|
|
587
|
+
"totalSize": 1,
|
|
588
|
+
"done": True,
|
|
589
|
+
"records": [
|
|
590
|
+
{
|
|
591
|
+
"Id": "0PG000000000001",
|
|
592
|
+
"DeveloperName": "PSG1",
|
|
593
|
+
}
|
|
594
|
+
],
|
|
595
|
+
},
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
responses.add(
|
|
599
|
+
method="GET",
|
|
600
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
601
|
+
status=400,
|
|
602
|
+
json=[{"errorCode": "INVALID_FIELD", "message": "Invalid field"}],
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
with pytest.raises(SalesforceException, match="Error querying Permission Sets"):
|
|
606
|
+
task()
|
|
607
|
+
|
|
608
|
+
def test_process_namespaces(self):
|
|
609
|
+
"""Test _process_namespaces"""
|
|
610
|
+
with mock.patch(
|
|
611
|
+
"cumulusci.tasks.salesforce.assign_ps_psg.inject_namespace",
|
|
612
|
+
return_value=("", "NS__PS1"),
|
|
613
|
+
):
|
|
614
|
+
task = create_task(
|
|
615
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
616
|
+
{"assignments": {"PSG1": ["PS1"]}, "namespace_inject": "NS"},
|
|
617
|
+
)
|
|
618
|
+
task._init_options({})
|
|
619
|
+
result = task._process_namespaces(["PS1"])
|
|
620
|
+
assert result == {"PS1": "NS__PS1"}
|
|
621
|
+
|
|
622
|
+
def test_build_name_conditions_no_namespace(self):
|
|
623
|
+
"""Test _build_name_conditions without namespace"""
|
|
624
|
+
conditions, mapping = build_name_conditions(["PS1"])
|
|
625
|
+
assert len(conditions) == 1
|
|
626
|
+
assert "Name = 'PS1'" in conditions[0]
|
|
627
|
+
assert ("PS1", None) in mapping
|
|
628
|
+
|
|
629
|
+
def test_build_name_conditions_with_namespace(self):
|
|
630
|
+
"""Test _build_name_conditions with namespace"""
|
|
631
|
+
conditions, mapping = build_name_conditions(["NS__PS1"])
|
|
632
|
+
assert len(conditions) == 1
|
|
633
|
+
assert "NamespacePrefix = 'NS' AND Name = 'PS1'" in conditions[0]
|
|
634
|
+
assert ("PS1", "NS") in mapping
|
|
635
|
+
|
|
636
|
+
def test_build_name_conditions_with_escaped_quotes(self):
|
|
637
|
+
"""Test _build_name_conditions with names containing quotes"""
|
|
638
|
+
conditions, mapping = build_name_conditions(["PS'1"])
|
|
639
|
+
assert "Name = 'PS''1'" in conditions[0] # Single quote should be escaped
|
|
640
|
+
|
|
641
|
+
def test_build_name_conditions_custom_field_name(self):
|
|
642
|
+
"""Test _build_name_conditions with custom field name"""
|
|
643
|
+
conditions, mapping = build_name_conditions(["PS1"], field_name="DeveloperName")
|
|
644
|
+
assert "DeveloperName = 'PS1'" in conditions[0]
|
|
645
|
+
|
|
646
|
+
@responses.activate
|
|
647
|
+
def test_create_permission_set_group_components_success(self):
|
|
648
|
+
"""Test _create_permission_set_group_components with success"""
|
|
649
|
+
task = create_task(
|
|
650
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
651
|
+
{"assignments": {"PSG1": ["PS1"]}},
|
|
652
|
+
)
|
|
653
|
+
task._init_options({})
|
|
654
|
+
task._init_task()
|
|
655
|
+
task.psg_ids = {"PSG1": "0PG000000000001"}
|
|
656
|
+
task.ps_ids = {"PS1": "0PS000000000001"}
|
|
657
|
+
|
|
658
|
+
records = [
|
|
659
|
+
{
|
|
660
|
+
"attributes": {"type": "PermissionSetGroupComponent"},
|
|
661
|
+
"PermissionSetGroupId": "0PG000000000001",
|
|
662
|
+
"PermissionSetId": "0PS000000000001",
|
|
663
|
+
}
|
|
664
|
+
]
|
|
665
|
+
|
|
666
|
+
responses.add(
|
|
667
|
+
method="POST",
|
|
668
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
|
|
669
|
+
status=200,
|
|
670
|
+
json=[{"id": "0PGC00000000001", "success": True, "errors": []}],
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
task._create_permission_set_group_components(records)
|
|
674
|
+
|
|
675
|
+
assert len(responses.calls) == 1
|
|
676
|
+
request_body = json.loads(responses.calls[0].request.body)
|
|
677
|
+
assert request_body["allOrNone"] is False
|
|
678
|
+
assert len(request_body["records"]) == 1
|
|
679
|
+
|
|
680
|
+
@responses.activate
|
|
681
|
+
def test_create_permission_set_group_components_with_errors(self):
|
|
682
|
+
"""Test _create_permission_set_group_components with duplicate errors (should not raise exception)"""
|
|
683
|
+
task = create_task(
|
|
684
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
685
|
+
{"assignments": {"PSG1": ["PS1"]}},
|
|
686
|
+
)
|
|
687
|
+
task._init_options({})
|
|
688
|
+
task._init_task()
|
|
689
|
+
task.psg_ids = {"PSG1": "0PG000000000001"}
|
|
690
|
+
task.ps_ids = {"PS1": "0PS000000000001"}
|
|
691
|
+
task.psg_names_sanitized = {"PSG1": "PSG1"}
|
|
692
|
+
task.ps_names_sanitized = {"PS1": "PS1"}
|
|
693
|
+
|
|
694
|
+
records = [
|
|
695
|
+
{
|
|
696
|
+
"attributes": {"type": "PermissionSetGroupComponent"},
|
|
697
|
+
"PermissionSetGroupId": "0PG000000000001",
|
|
698
|
+
"PermissionSetId": "0PS000000000001",
|
|
699
|
+
}
|
|
700
|
+
]
|
|
701
|
+
|
|
702
|
+
responses.add(
|
|
703
|
+
method="POST",
|
|
704
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
|
|
705
|
+
status=200,
|
|
706
|
+
json=[
|
|
707
|
+
{
|
|
708
|
+
"id": None,
|
|
709
|
+
"success": False,
|
|
710
|
+
"errors": [
|
|
711
|
+
{"message": "Duplicate value", "statusCode": "DUPLICATE_VALUE"}
|
|
712
|
+
],
|
|
713
|
+
}
|
|
714
|
+
],
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
# Duplicate errors are now handled gracefully and should not raise an exception
|
|
718
|
+
task._create_permission_set_group_components(records)
|
|
719
|
+
assert len(responses.calls) == 1
|
|
720
|
+
|
|
721
|
+
@responses.activate
|
|
722
|
+
def test_create_permission_set_group_components_api_error(self):
|
|
723
|
+
"""Test _create_permission_set_group_components with API error"""
|
|
724
|
+
task = create_task(
|
|
725
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
726
|
+
{"assignments": {"PSG1": ["PS1"]}},
|
|
727
|
+
)
|
|
728
|
+
task._init_options({})
|
|
729
|
+
task._init_task()
|
|
730
|
+
task.psg_ids = {"PSG1": "0PG000000000001"}
|
|
731
|
+
task.ps_ids = {"PS1": "0PS000000000001"}
|
|
732
|
+
|
|
733
|
+
records = [
|
|
734
|
+
{
|
|
735
|
+
"attributes": {"type": "PermissionSetGroupComponent"},
|
|
736
|
+
"PermissionSetGroupId": "0PG000000000001",
|
|
737
|
+
"PermissionSetId": "0PS000000000001",
|
|
738
|
+
}
|
|
739
|
+
]
|
|
740
|
+
|
|
741
|
+
responses.add(
|
|
742
|
+
method="POST",
|
|
743
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
|
|
744
|
+
status=500,
|
|
745
|
+
json=[{"errorCode": "INTERNAL_ERROR", "message": "Internal server error"}],
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
with pytest.raises(
|
|
749
|
+
SalesforceException, match="Error creating PermissionSetGroupComponent"
|
|
750
|
+
):
|
|
751
|
+
task._create_permission_set_group_components(records)
|
|
752
|
+
|
|
753
|
+
@responses.activate
|
|
754
|
+
def test_create_permission_set_group_components_non_list_response(self):
|
|
755
|
+
"""Test _create_permission_set_group_components with non-list response"""
|
|
756
|
+
task = create_task(
|
|
757
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
758
|
+
{"assignments": {"PSG1": ["PS1"]}},
|
|
759
|
+
)
|
|
760
|
+
task._init_options({})
|
|
761
|
+
task._init_task()
|
|
762
|
+
task.psg_ids = {"PSG1": "0PG000000000001"}
|
|
763
|
+
task.ps_ids = {"PS1": "0PS000000000001"}
|
|
764
|
+
|
|
765
|
+
records = [
|
|
766
|
+
{
|
|
767
|
+
"attributes": {"type": "PermissionSetGroupComponent"},
|
|
768
|
+
"PermissionSetGroupId": "0PG000000000001",
|
|
769
|
+
"PermissionSetId": "0PS000000000001",
|
|
770
|
+
}
|
|
771
|
+
]
|
|
772
|
+
|
|
773
|
+
responses.add(
|
|
774
|
+
method="POST",
|
|
775
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
|
|
776
|
+
status=200,
|
|
777
|
+
json={"id": "0PGC00000000001", "success": True, "errors": []},
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
# Should handle non-list response gracefully
|
|
781
|
+
task._create_permission_set_group_components(records)
|
|
782
|
+
assert len(responses.calls) == 1
|
|
783
|
+
|
|
784
|
+
@responses.activate
|
|
785
|
+
def test_create_permission_set_group_components_partial_success(self):
|
|
786
|
+
"""Test _create_permission_set_group_components with partial success (should not raise exception for DUPLICATE_VALUE errors)"""
|
|
787
|
+
task = create_task(
|
|
788
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
789
|
+
{"assignments": {"PSG1": ["PS1", "PS2"]}},
|
|
790
|
+
)
|
|
791
|
+
task._init_options({})
|
|
792
|
+
task._init_task()
|
|
793
|
+
task.psg_ids = {"PSG1": "0PG000000000001"}
|
|
794
|
+
task.ps_ids = {"PS1": "0PS000000000001", "PS2": "0PS000000000002"}
|
|
795
|
+
task.psg_names_sanitized = {"PSG1": "PSG1"}
|
|
796
|
+
task.ps_names_sanitized = {"PS1": "PS1", "PS2": "PS2"}
|
|
797
|
+
|
|
798
|
+
records = [
|
|
799
|
+
{
|
|
800
|
+
"attributes": {"type": "PermissionSetGroupComponent"},
|
|
801
|
+
"PermissionSetGroupId": "0PG000000000001",
|
|
802
|
+
"PermissionSetId": "0PS000000000001",
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
"attributes": {"type": "PermissionSetGroupComponent"},
|
|
806
|
+
"PermissionSetGroupId": "0PG000000000001",
|
|
807
|
+
"PermissionSetId": "0PS000000000002",
|
|
808
|
+
},
|
|
809
|
+
]
|
|
810
|
+
|
|
811
|
+
responses.add(
|
|
812
|
+
method="POST",
|
|
813
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
|
|
814
|
+
status=200,
|
|
815
|
+
json=[
|
|
816
|
+
{"id": "0PGC00000000001", "success": True, "errors": []},
|
|
817
|
+
{
|
|
818
|
+
"id": None,
|
|
819
|
+
"success": False,
|
|
820
|
+
"errors": [
|
|
821
|
+
{"message": "Duplicate value", "statusCode": "DUPLICATE_VALUE"}
|
|
822
|
+
],
|
|
823
|
+
},
|
|
824
|
+
],
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
task._create_permission_set_group_components(records)
|
|
828
|
+
assert len(responses.calls) == 1
|
|
829
|
+
|
|
830
|
+
@responses.activate
|
|
831
|
+
def test_run_task_multiple_psgs(self):
|
|
832
|
+
"""Test _run_task with multiple Permission Set Groups"""
|
|
833
|
+
task = create_task(
|
|
834
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
835
|
+
{"assignments": {"PSG1": ["PS1"], "PSG2": ["PS2"]}},
|
|
836
|
+
)
|
|
837
|
+
task._init_task()
|
|
838
|
+
|
|
839
|
+
# Mock PSG query
|
|
840
|
+
responses.add(
|
|
841
|
+
method="GET",
|
|
842
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
843
|
+
status=200,
|
|
844
|
+
json={
|
|
845
|
+
"totalSize": 2,
|
|
846
|
+
"done": True,
|
|
847
|
+
"records": [
|
|
848
|
+
{
|
|
849
|
+
"Id": "0PG000000000001",
|
|
850
|
+
"DeveloperName": "PSG1",
|
|
851
|
+
"NamespacePrefix": None,
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
"Id": "0PG000000000002",
|
|
855
|
+
"DeveloperName": "PSG2",
|
|
856
|
+
"NamespacePrefix": None,
|
|
857
|
+
},
|
|
858
|
+
],
|
|
859
|
+
},
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
# Mock PS query
|
|
863
|
+
responses.add(
|
|
864
|
+
method="GET",
|
|
865
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
866
|
+
status=200,
|
|
867
|
+
json={
|
|
868
|
+
"totalSize": 2,
|
|
869
|
+
"done": True,
|
|
870
|
+
"records": [
|
|
871
|
+
{"Id": "0PS000000000001", "Name": "PS1", "NamespacePrefix": None},
|
|
872
|
+
{"Id": "0PS000000000002", "Name": "PS2", "NamespacePrefix": None},
|
|
873
|
+
],
|
|
874
|
+
},
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
# Mock Composite API
|
|
878
|
+
responses.add(
|
|
879
|
+
method="POST",
|
|
880
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
|
|
881
|
+
status=200,
|
|
882
|
+
json=[
|
|
883
|
+
{"id": "0PGC00000000001", "success": True, "errors": []},
|
|
884
|
+
{"id": "0PGC00000000002", "success": True, "errors": []},
|
|
885
|
+
],
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
task._run_task()
|
|
889
|
+
|
|
890
|
+
assert len(responses.calls) == 3
|
|
891
|
+
composite_request = json.loads(responses.calls[2].request.body)
|
|
892
|
+
assert len(composite_request["records"]) == 2
|
|
893
|
+
|
|
894
|
+
@responses.activate
|
|
895
|
+
def test_run_task_batch_error_with_fail_on_error_true(self):
|
|
896
|
+
"""Test _run_task raises exception when batch fails and fail_on_error=True"""
|
|
897
|
+
task = create_task(
|
|
898
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
899
|
+
{"assignments": {"PSG1": ["PS1"]}, "fail_on_error": True},
|
|
900
|
+
)
|
|
901
|
+
task._init_task()
|
|
902
|
+
|
|
903
|
+
# Mock PSG query
|
|
904
|
+
responses.add(
|
|
905
|
+
method="GET",
|
|
906
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
907
|
+
status=200,
|
|
908
|
+
json={
|
|
909
|
+
"totalSize": 1,
|
|
910
|
+
"done": True,
|
|
911
|
+
"records": [
|
|
912
|
+
{
|
|
913
|
+
"Id": "0PG000000000001",
|
|
914
|
+
"DeveloperName": "PSG1",
|
|
915
|
+
"NamespacePrefix": None,
|
|
916
|
+
}
|
|
917
|
+
],
|
|
918
|
+
},
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
# Mock PS query
|
|
922
|
+
responses.add(
|
|
923
|
+
method="GET",
|
|
924
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
925
|
+
status=200,
|
|
926
|
+
json={
|
|
927
|
+
"totalSize": 1,
|
|
928
|
+
"done": True,
|
|
929
|
+
"records": [
|
|
930
|
+
{"Id": "0PS000000000001", "Name": "PS1", "NamespacePrefix": None}
|
|
931
|
+
],
|
|
932
|
+
},
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
# Mock Composite API to raise an exception
|
|
936
|
+
responses.add(
|
|
937
|
+
method="POST",
|
|
938
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
|
|
939
|
+
status=500,
|
|
940
|
+
json={"errorCode": "INTERNAL_ERROR", "message": "Internal server error"},
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
with pytest.raises(SalesforceException):
|
|
944
|
+
task._run_task()
|
|
945
|
+
|
|
946
|
+
@responses.activate
|
|
947
|
+
def test_run_task_batch_error_with_fail_on_error_false(self):
|
|
948
|
+
"""Test _run_task continues when batch fails and fail_on_error=False"""
|
|
949
|
+
task = create_task(
|
|
950
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
951
|
+
{"assignments": {"PSG1": ["PS1"]}, "fail_on_error": False},
|
|
952
|
+
)
|
|
953
|
+
task._init_task()
|
|
954
|
+
|
|
955
|
+
# Mock PSG query
|
|
956
|
+
responses.add(
|
|
957
|
+
method="GET",
|
|
958
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
959
|
+
status=200,
|
|
960
|
+
json={
|
|
961
|
+
"totalSize": 1,
|
|
962
|
+
"done": True,
|
|
963
|
+
"records": [
|
|
964
|
+
{
|
|
965
|
+
"Id": "0PG000000000001",
|
|
966
|
+
"DeveloperName": "PSG1",
|
|
967
|
+
"NamespacePrefix": None,
|
|
968
|
+
}
|
|
969
|
+
],
|
|
970
|
+
},
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
# Mock PS query
|
|
974
|
+
responses.add(
|
|
975
|
+
method="GET",
|
|
976
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
977
|
+
status=200,
|
|
978
|
+
json={
|
|
979
|
+
"totalSize": 1,
|
|
980
|
+
"done": True,
|
|
981
|
+
"records": [
|
|
982
|
+
{"Id": "0PS000000000001", "Name": "PS1", "NamespacePrefix": None}
|
|
983
|
+
],
|
|
984
|
+
},
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
# Mock Composite API to raise an exception
|
|
988
|
+
responses.add(
|
|
989
|
+
method="POST",
|
|
990
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
|
|
991
|
+
status=500,
|
|
992
|
+
json={"errorCode": "INTERNAL_ERROR", "message": "Internal server error"},
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
# Should not raise, just log the error
|
|
996
|
+
task._run_task()
|
|
997
|
+
|
|
998
|
+
# Verify that the error was logged (we can't easily test logging, but we can verify
|
|
999
|
+
# that the task completed without raising)
|
|
1000
|
+
assert len(responses.calls) == 3
|
|
1001
|
+
|
|
1002
|
+
@responses.activate
|
|
1003
|
+
def test_run_task_batch_error_with_fail_on_error_default(self):
|
|
1004
|
+
"""Test _run_task continues when batch fails and fail_on_error is default (False)"""
|
|
1005
|
+
task = create_task(
|
|
1006
|
+
AssignPermissionSetToPermissionSetGroup,
|
|
1007
|
+
{"assignments": {"PSG1": ["PS1"]}, "fail_on_error": False},
|
|
1008
|
+
)
|
|
1009
|
+
task._init_task()
|
|
1010
|
+
|
|
1011
|
+
# Mock PSG query
|
|
1012
|
+
responses.add(
|
|
1013
|
+
method="GET",
|
|
1014
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
1015
|
+
status=200,
|
|
1016
|
+
json={
|
|
1017
|
+
"totalSize": 1,
|
|
1018
|
+
"done": True,
|
|
1019
|
+
"records": [
|
|
1020
|
+
{
|
|
1021
|
+
"Id": "0PG000000000001",
|
|
1022
|
+
"DeveloperName": "PSG1",
|
|
1023
|
+
"NamespacePrefix": None,
|
|
1024
|
+
}
|
|
1025
|
+
],
|
|
1026
|
+
},
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
# Mock PS query
|
|
1030
|
+
responses.add(
|
|
1031
|
+
method="GET",
|
|
1032
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/query/",
|
|
1033
|
+
status=200,
|
|
1034
|
+
json={
|
|
1035
|
+
"totalSize": 1,
|
|
1036
|
+
"done": True,
|
|
1037
|
+
"records": [
|
|
1038
|
+
{"Id": "0PS000000000001", "Name": "PS1", "NamespacePrefix": None}
|
|
1039
|
+
],
|
|
1040
|
+
},
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
# Mock Composite API to raise an exception
|
|
1044
|
+
responses.add(
|
|
1045
|
+
method="POST",
|
|
1046
|
+
url=f"{task.org_config.instance_url}/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
|
|
1047
|
+
status=500,
|
|
1048
|
+
json={"errorCode": "INTERNAL_ERROR", "message": "Internal server error"},
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
# Should not raise, just log the error (fail_on_error defaults to False)
|
|
1052
|
+
task._run_task()
|
|
1053
|
+
|
|
1054
|
+
# Verify that the error was logged but task completed
|
|
1055
|
+
assert len(responses.calls) == 3
|