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.
Files changed (135) hide show
  1. cumulusci/__about__.py +1 -1
  2. cumulusci/cli/logger.py +2 -2
  3. cumulusci/cli/service.py +20 -0
  4. cumulusci/cli/task.py +19 -3
  5. cumulusci/cli/tests/test_error.py +3 -1
  6. cumulusci/cli/tests/test_flow.py +279 -2
  7. cumulusci/cli/tests/test_org.py +5 -0
  8. cumulusci/cli/tests/test_service.py +15 -12
  9. cumulusci/cli/tests/test_task.py +122 -2
  10. cumulusci/cli/tests/utils.py +1 -4
  11. cumulusci/core/config/__init__.py +1 -0
  12. cumulusci/core/config/base_task_flow_config.py +26 -1
  13. cumulusci/core/config/org_config.py +2 -1
  14. cumulusci/core/config/project_config.py +14 -20
  15. cumulusci/core/config/scratch_org_config.py +12 -0
  16. cumulusci/core/config/tests/test_config.py +1 -0
  17. cumulusci/core/config/tests/test_config_expensive.py +9 -3
  18. cumulusci/core/config/universal_config.py +3 -4
  19. cumulusci/core/dependencies/base.py +5 -1
  20. cumulusci/core/dependencies/dependencies.py +1 -1
  21. cumulusci/core/dependencies/github.py +1 -2
  22. cumulusci/core/dependencies/resolvers.py +1 -1
  23. cumulusci/core/dependencies/tests/test_dependencies.py +1 -1
  24. cumulusci/core/dependencies/tests/test_resolvers.py +1 -1
  25. cumulusci/core/flowrunner.py +90 -6
  26. cumulusci/core/github.py +1 -1
  27. cumulusci/core/sfdx.py +3 -1
  28. cumulusci/core/source_transforms/tests/test_transforms.py +1 -1
  29. cumulusci/core/source_transforms/transforms.py +1 -1
  30. cumulusci/core/tasks.py +13 -2
  31. cumulusci/core/tests/test_flowrunner.py +100 -0
  32. cumulusci/core/tests/test_tasks.py +65 -0
  33. cumulusci/core/utils.py +3 -1
  34. cumulusci/core/versions.py +1 -1
  35. cumulusci/cumulusci.yml +73 -1
  36. cumulusci/oauth/client.py +1 -1
  37. cumulusci/plugins/plugin_base.py +5 -3
  38. cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
  39. cumulusci/salesforce_api/rest_deploy.py +1 -1
  40. cumulusci/schema/cumulusci.jsonschema.json +69 -0
  41. cumulusci/tasks/apex/anon.py +1 -1
  42. cumulusci/tasks/apex/testrunner.py +421 -144
  43. cumulusci/tasks/apex/tests/test_apex_tasks.py +917 -1
  44. cumulusci/tasks/bulkdata/extract.py +0 -1
  45. cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +1 -1
  46. cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +1 -1
  47. cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +1 -1
  48. cumulusci/tasks/bulkdata/generate_and_load_data.py +136 -12
  49. cumulusci/tasks/bulkdata/mapping_parser.py +139 -44
  50. cumulusci/tasks/bulkdata/select_utils.py +1 -1
  51. cumulusci/tasks/bulkdata/snowfakery.py +100 -25
  52. cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +159 -0
  53. cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
  54. cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +763 -1
  55. cumulusci/tasks/bulkdata/tests/test_select_utils.py +46 -0
  56. cumulusci/tasks/bulkdata/tests/test_snowfakery.py +133 -0
  57. cumulusci/tasks/create_package_version.py +190 -16
  58. cumulusci/tasks/datadictionary.py +1 -1
  59. cumulusci/tasks/metadata_etl/__init__.py +2 -0
  60. cumulusci/tasks/metadata_etl/applications.py +256 -0
  61. cumulusci/tasks/metadata_etl/base.py +7 -3
  62. cumulusci/tasks/metadata_etl/layouts.py +1 -1
  63. cumulusci/tasks/metadata_etl/permissions.py +1 -1
  64. cumulusci/tasks/metadata_etl/remote_site_settings.py +2 -2
  65. cumulusci/tasks/metadata_etl/tests/test_applications.py +710 -0
  66. cumulusci/tasks/push/README.md +15 -17
  67. cumulusci/tasks/release_notes/README.md +13 -13
  68. cumulusci/tasks/release_notes/generator.py +13 -8
  69. cumulusci/tasks/robotframework/tests/test_robotframework.py +6 -1
  70. cumulusci/tasks/salesforce/Deploy.py +53 -2
  71. cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
  72. cumulusci/tasks/salesforce/__init__.py +1 -0
  73. cumulusci/tasks/salesforce/assign_ps_psg.py +448 -0
  74. cumulusci/tasks/salesforce/composite.py +1 -1
  75. cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
  76. cumulusci/tasks/salesforce/enable_prediction.py +5 -1
  77. cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
  78. cumulusci/tasks/salesforce/insert_record.py +18 -19
  79. cumulusci/tasks/salesforce/sourcetracking.py +1 -1
  80. cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
  81. cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
  82. cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
  83. cumulusci/tasks/salesforce/tests/test_enable_prediction.py +4 -2
  84. cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
  85. cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
  86. cumulusci/tasks/salesforce/tests/test_update_external_auth_identity_provider.py +927 -0
  87. cumulusci/tasks/salesforce/tests/test_update_external_credential.py +1427 -0
  88. cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
  89. cumulusci/tasks/salesforce/tests/test_update_record.py +512 -0
  90. cumulusci/tasks/salesforce/update_dependencies.py +2 -2
  91. cumulusci/tasks/salesforce/update_external_auth_identity_provider.py +551 -0
  92. cumulusci/tasks/salesforce/update_external_credential.py +647 -0
  93. cumulusci/tasks/salesforce/update_named_credential.py +441 -0
  94. cumulusci/tasks/salesforce/update_profile.py +17 -13
  95. cumulusci/tasks/salesforce/update_record.py +217 -0
  96. cumulusci/tasks/salesforce/users/permsets.py +62 -5
  97. cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
  98. cumulusci/tasks/sfdmu/__init__.py +0 -0
  99. cumulusci/tasks/sfdmu/sfdmu.py +376 -0
  100. cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
  101. cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
  102. cumulusci/tasks/sfdmu/tests/test_sfdmu.py +1012 -0
  103. cumulusci/tasks/tests/test_create_package_version.py +716 -1
  104. cumulusci/tasks/tests/test_util.py +42 -0
  105. cumulusci/tasks/util.py +37 -1
  106. cumulusci/tasks/utility/copyContents.py +402 -0
  107. cumulusci/tasks/utility/credentialManager.py +302 -0
  108. cumulusci/tasks/utility/directoryRecreator.py +30 -0
  109. cumulusci/tasks/utility/env_management.py +1 -1
  110. cumulusci/tasks/utility/secretsToEnv.py +135 -0
  111. cumulusci/tasks/utility/tests/test_copyContents.py +1719 -0
  112. cumulusci/tasks/utility/tests/test_credentialManager.py +1150 -0
  113. cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
  114. cumulusci/tasks/utility/tests/test_secretsToEnv.py +1118 -0
  115. cumulusci/tests/test_integration_infrastructure.py +3 -1
  116. cumulusci/tests/test_utils.py +70 -6
  117. cumulusci/utils/__init__.py +54 -9
  118. cumulusci/utils/classutils.py +5 -2
  119. cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
  120. cumulusci/utils/options.py +23 -1
  121. cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
  122. cumulusci/utils/yaml/cumulusci_yml.py +8 -3
  123. cumulusci/utils/yaml/model_parser.py +2 -2
  124. cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
  125. cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
  126. cumulusci/vcs/base.py +23 -15
  127. cumulusci/vcs/bootstrap.py +5 -4
  128. cumulusci/vcs/utils/list_modified_files.py +189 -0
  129. cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
  130. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/METADATA +11 -10
  131. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/RECORD +135 -104
  132. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/WHEEL +1 -1
  133. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/entry_points.txt +0 -0
  134. {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/AUTHORS.rst +0 -0
  135. {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