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,1042 @@
1
+ import os
2
+ from unittest import mock
3
+
4
+ import pytest
5
+ import responses
6
+
7
+ from cumulusci.core.exceptions import SalesforceDXException
8
+ from cumulusci.tasks.salesforce.update_named_credential import (
9
+ NamedCredentialCalloutOptions,
10
+ NamedCredentialHttpHeader,
11
+ NamedCredentialParameter,
12
+ TransformNamedCredentialParameter,
13
+ UpdateNamedCredential,
14
+ )
15
+ from cumulusci.tests.util import CURRENT_SF_API_VERSION
16
+
17
+ from .util import create_task
18
+
19
+
20
+ class TestNamedCredentialHttpHeader:
21
+ """Test NamedCredentialHttpHeader model"""
22
+
23
+ def test_http_header_defaults(self):
24
+ """Test default values for http header"""
25
+ header = NamedCredentialHttpHeader(name="test-header", value="test-value")
26
+ assert header.name == "test-header"
27
+ assert header.value == "test-value"
28
+ assert header.secret is False
29
+ assert header.sequence_number is None
30
+
31
+ def test_http_header_with_secret(self):
32
+ """Test http header with secret flag"""
33
+ header = NamedCredentialHttpHeader(
34
+ name="api-key", value="secret123", secret=True, sequence_number=1
35
+ )
36
+ assert header.name == "api-key"
37
+ assert header.value == "secret123"
38
+ assert header.secret is True
39
+ assert header.sequence_number == 1
40
+
41
+
42
+ class TestNamedCredentialParameter:
43
+ """Test NamedCredentialParameter model"""
44
+
45
+ def test_parameter_with_url(self):
46
+ """Test parameter with URL"""
47
+ param = NamedCredentialParameter(url="https://example.com")
48
+ assert param.url == "https://example.com"
49
+ assert param.param_type() == "Url"
50
+ assert param.param_value() == "https://example.com"
51
+
52
+ def test_parameter_with_authentication(self):
53
+ """Test parameter with authentication"""
54
+ param = NamedCredentialParameter(authentication="MyAuth")
55
+ assert param.authentication == "MyAuth"
56
+ assert param.param_type() == "Authentication"
57
+ assert param.param_value() == "MyAuth"
58
+
59
+ def test_parameter_with_certificate(self):
60
+ """Test parameter with certificate"""
61
+ param = NamedCredentialParameter(certificate="MyCert")
62
+ assert param.certificate == "MyCert"
63
+ assert param.param_type() == "ClientCertificate"
64
+ assert param.param_value() == "MyCert"
65
+
66
+ def test_parameter_with_allowed_namespaces(self):
67
+ """Test parameter with allowed managed package namespaces"""
68
+ param = NamedCredentialParameter(
69
+ allowed_managed_package_namespaces="namespace1"
70
+ )
71
+ assert param.allowed_managed_package_namespaces == "namespace1"
72
+ assert param.param_type() == "AllowedManagedPackageNamespaces"
73
+ assert param.param_value() == "namespace1"
74
+
75
+ def test_parameter_with_http_headers(self):
76
+ """Test parameter with HTTP headers"""
77
+ headers = [
78
+ NamedCredentialHttpHeader(name="header1", value="value1"),
79
+ NamedCredentialHttpHeader(
80
+ name="header2", value="value2", sequence_number=1
81
+ ),
82
+ ]
83
+ param = NamedCredentialParameter(http_header=headers)
84
+ assert param.param_type() == "HttpHeader"
85
+ assert param.param_value(http_header="header1") == "value1"
86
+ assert param.param_value(http_header="header2") == "value2"
87
+ assert param.param_value(http_header="nonexistent") is None
88
+
89
+ def test_parameter_validation_error_multiple_params(self):
90
+ """Test that only one parameter can be provided"""
91
+ with pytest.raises(ValueError, match="Only one of the parameters is required"):
92
+ NamedCredentialParameter(url="https://example.com", certificate="MyCert")
93
+
94
+ def test_parameter_validation_error_no_params(self):
95
+ """Test that at least one parameter must be provided"""
96
+ with pytest.raises(ValueError, match="Only one of the parameters is required"):
97
+ NamedCredentialParameter()
98
+
99
+ def test_get_parameter_to_update_url(self):
100
+ """Test get_parameter_to_update for URL parameter"""
101
+ param = NamedCredentialParameter(url="https://example.com")
102
+ result = param.get_parameter_to_update()
103
+ assert len(result) == 1
104
+ assert result[0]["parameterType"] == "Url"
105
+ assert result[0]["parameterValue"] == "https://example.com"
106
+
107
+ def test_get_parameter_to_update_authentication(self):
108
+ """Test get_parameter_to_update for authentication parameter"""
109
+ param = NamedCredentialParameter(authentication="MyAuth")
110
+ result = param.get_parameter_to_update()
111
+ assert len(result) == 1
112
+ assert result[0]["parameterType"] == "Authentication"
113
+ assert result[0]["parameterName"] == "ExternalCredential"
114
+ assert result[0]["externalCredential"] == "MyAuth"
115
+ assert result[0]["parameterValue"] == "MyAuth"
116
+
117
+ def test_get_parameter_to_update_certificate(self):
118
+ """Test get_parameter_to_update for certificate parameter"""
119
+ param = NamedCredentialParameter(certificate="MyCert")
120
+ result = param.get_parameter_to_update()
121
+ assert len(result) == 1
122
+ assert result[0]["parameterType"] == "ClientCertificate"
123
+ assert result[0]["parameterName"] == "ClientCertificate"
124
+ assert result[0]["certificate"] == "MyCert"
125
+ assert result[0]["parameterValue"] == "MyCert"
126
+
127
+ def test_get_parameter_to_update_http_headers(self):
128
+ """Test get_parameter_to_update for HTTP headers"""
129
+ headers = [
130
+ NamedCredentialHttpHeader(name="header1", value="value1", secret=True),
131
+ NamedCredentialHttpHeader(
132
+ name="header2", value="value2", sequence_number=2
133
+ ),
134
+ ]
135
+ param = NamedCredentialParameter(http_header=headers)
136
+ result = param.get_parameter_to_update()
137
+ assert len(result) == 2
138
+ assert result[0]["parameterType"] == "HttpHeader"
139
+ assert result[0]["parameterName"] == "header1"
140
+ assert result[0]["parameterValue"] == "value1"
141
+ assert result[0]["secret"] is True
142
+ assert result[1]["parameterName"] == "header2"
143
+ assert result[1]["parameterValue"] == "value2"
144
+ assert result[1]["sequenceNumber"] == 2
145
+
146
+
147
+ class TestTransformNamedCredentialParameter:
148
+ """Test TransformNamedCredentialParameter model"""
149
+
150
+ def test_transform_param_value_url(self):
151
+ """Test transform parameter value for URL from environment"""
152
+ with mock.patch.dict(os.environ, {"TEST_URL": "https://env-example.com"}):
153
+ param = TransformNamedCredentialParameter(url="TEST_URL")
154
+ assert param.param_value() == "https://env-example.com"
155
+
156
+ def test_transform_param_value_authentication(self):
157
+ """Test transform parameter value for authentication from environment"""
158
+ with mock.patch.dict(os.environ, {"TEST_AUTH": "EnvAuth"}):
159
+ param = TransformNamedCredentialParameter(authentication="TEST_AUTH")
160
+ assert param.param_value() == "EnvAuth"
161
+
162
+ def test_transform_param_value_certificate(self):
163
+ """Test transform parameter value for certificate from environment"""
164
+ with mock.patch.dict(os.environ, {"TEST_CERT": "EnvCert"}):
165
+ param = TransformNamedCredentialParameter(certificate="TEST_CERT")
166
+ assert param.param_value() == "EnvCert"
167
+
168
+ def test_transform_param_value_http_header(self):
169
+ """Test transform parameter value for HTTP header from environment"""
170
+ with mock.patch.dict(os.environ, {"HEADER_VALUE": "env-header-value"}):
171
+ headers = [
172
+ NamedCredentialHttpHeader(name="test-header", value="HEADER_VALUE")
173
+ ]
174
+ param = TransformNamedCredentialParameter(http_header=headers)
175
+ assert param.param_value(http_header="test-header") == "env-header-value"
176
+
177
+ def test_transform_param_value_missing_env(self):
178
+ """Test transform parameter value when environment variable is missing"""
179
+ param = TransformNamedCredentialParameter(url="NONEXISTENT_VAR")
180
+ assert param.param_value() is None
181
+
182
+
183
+ class TestNamedCredentialCalloutOptions:
184
+ """Test NamedCredentialCalloutOptions model"""
185
+
186
+ def test_callout_options_defaults(self):
187
+ """Test default values for callout options"""
188
+ options = NamedCredentialCalloutOptions()
189
+ assert options.allow_merge_fields_in_body is None
190
+ assert options.allow_merge_fields_in_header is None
191
+ assert options.generate_authorization_header is None
192
+
193
+ def test_callout_options_with_values(self):
194
+ """Test callout options with values"""
195
+ options = NamedCredentialCalloutOptions(
196
+ allow_merge_fields_in_body=True,
197
+ allow_merge_fields_in_header=True,
198
+ generate_authorization_header=False,
199
+ )
200
+ assert options.allow_merge_fields_in_body is True
201
+ assert options.allow_merge_fields_in_header is True
202
+ assert options.generate_authorization_header is False
203
+
204
+
205
+ class TestUpdateNamedCredential:
206
+ """Test UpdateNamedCredential task"""
207
+
208
+ @responses.activate
209
+ def test_update_named_credential_success(self):
210
+ """Test successful update of named credential"""
211
+ task = create_task(
212
+ UpdateNamedCredential,
213
+ {
214
+ "name": "testNc",
215
+ "namespace": "",
216
+ "callout_options": {
217
+ "allow_merge_fields_in_body": True,
218
+ "allow_merge_fields_in_header": True,
219
+ "generate_authorization_header": True,
220
+ },
221
+ "parameters": [{"url": "https://testingAPI.example.com"}],
222
+ },
223
+ )
224
+
225
+ nc_id = "0XA1234567890ABC"
226
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
227
+
228
+ # Mock query for named credential ID
229
+ responses.add(
230
+ method="GET",
231
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+LIMIT+1",
232
+ json={"size": 1, "records": [{"Id": nc_id}]},
233
+ status=200,
234
+ )
235
+
236
+ # Mock get named credential object
237
+ responses.add(
238
+ method="GET",
239
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
240
+ json={
241
+ "Metadata": {
242
+ "namedCredentialType": "SecuredEndpoint",
243
+ "namedCredentialParameters": [
244
+ {
245
+ "parameterName": None,
246
+ "parameterType": "Url",
247
+ "parameterValue": "https://old.example.com",
248
+ "certificate": None,
249
+ "description": None,
250
+ "externalCredential": None,
251
+ "sequenceNumber": None,
252
+ }
253
+ ],
254
+ }
255
+ },
256
+ status=200,
257
+ )
258
+
259
+ # Mock update named credential
260
+ responses.add(
261
+ method="PATCH",
262
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
263
+ json={},
264
+ status=200,
265
+ )
266
+
267
+ task()
268
+ assert len(responses.calls) == 3
269
+
270
+ @responses.activate
271
+ def test_update_named_credential_with_namespace(self):
272
+ """Test update of named credential with namespace"""
273
+ task = create_task(
274
+ UpdateNamedCredential,
275
+ {
276
+ "name": "testNc",
277
+ "namespace": "th_dev",
278
+ "parameters": [{"url": "https://testingAPI.example.com"}],
279
+ },
280
+ )
281
+
282
+ nc_id = "0XA1234567890ABC"
283
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
284
+
285
+ # Mock query for named credential ID with namespace
286
+ responses.add(
287
+ method="GET",
288
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+AND+NamespacePrefix%3D%27th_dev%27+LIMIT+1",
289
+ json={"size": 1, "records": [{"Id": nc_id}]},
290
+ status=200,
291
+ )
292
+
293
+ # Mock get named credential object
294
+ responses.add(
295
+ method="GET",
296
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
297
+ json={
298
+ "Metadata": {
299
+ "namedCredentialType": "SecuredEndpoint",
300
+ "namedCredentialParameters": [
301
+ {
302
+ "parameterName": None,
303
+ "parameterType": "Url",
304
+ "parameterValue": "https://old.example.com",
305
+ }
306
+ ],
307
+ }
308
+ },
309
+ status=200,
310
+ )
311
+
312
+ # Mock update named credential
313
+ responses.add(
314
+ method="PATCH",
315
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
316
+ json={},
317
+ status=200,
318
+ )
319
+
320
+ task()
321
+ assert len(responses.calls) == 3
322
+
323
+ @responses.activate
324
+ def test_update_named_credential_not_found(self):
325
+ """Test error when named credential is not found"""
326
+ task = create_task(
327
+ UpdateNamedCredential,
328
+ {
329
+ "name": "nonexistent",
330
+ "parameters": [{"url": "https://testingAPI.example.com"}],
331
+ },
332
+ )
333
+
334
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
335
+
336
+ # Mock query for named credential ID - not found
337
+ responses.add(
338
+ method="GET",
339
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27nonexistent%27+LIMIT+1",
340
+ json={"size": 0, "records": []},
341
+ status=200,
342
+ )
343
+
344
+ with pytest.raises(Exception): # Can be TypeError or SalesforceDXException
345
+ task()
346
+
347
+ @responses.activate
348
+ def test_update_named_credential_query_error(self):
349
+ """Test error handling when query fails"""
350
+ task = create_task(
351
+ UpdateNamedCredential,
352
+ {
353
+ "name": "testNc",
354
+ "parameters": [{"url": "https://testingAPI.example.com"}],
355
+ },
356
+ )
357
+
358
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
359
+
360
+ # Mock query error
361
+ responses.add(
362
+ method="GET",
363
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+LIMIT+1",
364
+ json={"error": "Query failed"},
365
+ status=500,
366
+ )
367
+
368
+ with pytest.raises(Exception): # Can be TypeError or SalesforceDXException
369
+ task()
370
+
371
+ @responses.activate
372
+ def test_update_named_credential_not_secured_endpoint(self):
373
+ """Test error when named credential is not a SecuredEndpoint"""
374
+ task = create_task(
375
+ UpdateNamedCredential,
376
+ {
377
+ "name": "testNc",
378
+ "parameters": [{"url": "https://testingAPI.example.com"}],
379
+ },
380
+ )
381
+
382
+ nc_id = "0XA1234567890ABC"
383
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
384
+
385
+ # Mock query for named credential ID
386
+ responses.add(
387
+ method="GET",
388
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+LIMIT+1",
389
+ json={"size": 1, "records": [{"Id": nc_id}]},
390
+ status=200,
391
+ )
392
+
393
+ # Mock get named credential object with wrong type
394
+ responses.add(
395
+ method="GET",
396
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
397
+ json={"Metadata": {"namedCredentialType": "Legacy"}},
398
+ status=200,
399
+ )
400
+
401
+ with pytest.raises(
402
+ SalesforceDXException,
403
+ match="Named credential 'testNc' is not a secured endpoint",
404
+ ):
405
+ task()
406
+
407
+ @responses.activate
408
+ def test_update_named_credential_get_object_error(self):
409
+ """Test error when getting named credential object fails"""
410
+ task = create_task(
411
+ UpdateNamedCredential,
412
+ {
413
+ "name": "testNc",
414
+ "parameters": [{"url": "https://testingAPI.example.com"}],
415
+ },
416
+ )
417
+
418
+ nc_id = "0XA1234567890ABC"
419
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
420
+
421
+ # Mock query for named credential ID
422
+ responses.add(
423
+ method="GET",
424
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+LIMIT+1",
425
+ json={"size": 1, "records": [{"Id": nc_id}]},
426
+ status=200,
427
+ )
428
+
429
+ # Mock get named credential object - error
430
+ responses.add(
431
+ method="GET",
432
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
433
+ json={"error": "Failed to retrieve"},
434
+ status=404,
435
+ )
436
+
437
+ with pytest.raises(Exception): # Can be TypeError or SalesforceDXException
438
+ task()
439
+
440
+ @responses.activate
441
+ def test_update_named_credential_update_error(self):
442
+ """Test error when updating named credential fails"""
443
+ task = create_task(
444
+ UpdateNamedCredential,
445
+ {
446
+ "name": "testNc",
447
+ "parameters": [{"url": "https://testingAPI.example.com"}],
448
+ },
449
+ )
450
+
451
+ nc_id = "0XA1234567890ABC"
452
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
453
+
454
+ # Mock query for named credential ID
455
+ responses.add(
456
+ method="GET",
457
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+LIMIT+1",
458
+ json={"size": 1, "records": [{"Id": nc_id}]},
459
+ status=200,
460
+ )
461
+
462
+ # Mock get named credential object
463
+ responses.add(
464
+ method="GET",
465
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
466
+ json={
467
+ "Metadata": {
468
+ "namedCredentialType": "SecuredEndpoint",
469
+ "namedCredentialParameters": [
470
+ {
471
+ "parameterName": None,
472
+ "parameterType": "Url",
473
+ "parameterValue": "https://old.example.com",
474
+ }
475
+ ],
476
+ }
477
+ },
478
+ status=200,
479
+ )
480
+
481
+ # Mock update named credential - error
482
+ responses.add(
483
+ method="PATCH",
484
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
485
+ json={"error": "Update failed"},
486
+ status=400,
487
+ )
488
+
489
+ with pytest.raises(Exception): # Can be TypeError or SalesforceDXException
490
+ task()
491
+
492
+ @responses.activate
493
+ def test_update_named_credential_with_http_headers(self):
494
+ """Test update of named credential with HTTP headers"""
495
+ task = create_task(
496
+ UpdateNamedCredential,
497
+ {
498
+ "name": "testNc",
499
+ "parameters": [
500
+ {
501
+ "http_header": [
502
+ {"name": "x-api-key", "value": "secret123", "secret": True},
503
+ {
504
+ "name": "x-client-id",
505
+ "value": "client456",
506
+ "sequence_number": 1,
507
+ },
508
+ ]
509
+ }
510
+ ],
511
+ },
512
+ )
513
+
514
+ nc_id = "0XA1234567890ABC"
515
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
516
+
517
+ # Mock query for named credential ID
518
+ responses.add(
519
+ method="GET",
520
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+LIMIT+1",
521
+ json={"size": 1, "records": [{"Id": nc_id}]},
522
+ status=200,
523
+ )
524
+
525
+ # Mock get named credential object
526
+ responses.add(
527
+ method="GET",
528
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
529
+ json={
530
+ "Metadata": {
531
+ "namedCredentialType": "SecuredEndpoint",
532
+ "namedCredentialParameters": [
533
+ {
534
+ "parameterName": None,
535
+ "parameterType": "Url",
536
+ "parameterValue": "https://api.example.com",
537
+ }
538
+ ],
539
+ }
540
+ },
541
+ status=200,
542
+ )
543
+
544
+ # Mock update named credential
545
+ responses.add(
546
+ method="PATCH",
547
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
548
+ json={},
549
+ status=200,
550
+ )
551
+
552
+ task()
553
+ assert len(responses.calls) == 3
554
+
555
+ @responses.activate
556
+ def test_update_named_credential_with_authentication(self):
557
+ """Test update of named credential with authentication"""
558
+ task = create_task(
559
+ UpdateNamedCredential,
560
+ {
561
+ "name": "testNc",
562
+ "parameters": [{"authentication": "MyExternalCredential"}],
563
+ },
564
+ )
565
+
566
+ nc_id = "0XA1234567890ABC"
567
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
568
+
569
+ # Mock query for named credential ID
570
+ responses.add(
571
+ method="GET",
572
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+LIMIT+1",
573
+ json={"size": 1, "records": [{"Id": nc_id}]},
574
+ status=200,
575
+ )
576
+
577
+ # Mock get named credential object
578
+ responses.add(
579
+ method="GET",
580
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
581
+ json={
582
+ "Metadata": {
583
+ "namedCredentialType": "SecuredEndpoint",
584
+ "namedCredentialParameters": [
585
+ {
586
+ "parameterName": None,
587
+ "parameterType": "Url",
588
+ "parameterValue": "https://api.example.com",
589
+ }
590
+ ],
591
+ }
592
+ },
593
+ status=200,
594
+ )
595
+
596
+ # Mock update named credential
597
+ responses.add(
598
+ method="PATCH",
599
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
600
+ json={},
601
+ status=200,
602
+ )
603
+
604
+ task()
605
+ assert len(responses.calls) == 3
606
+
607
+ @responses.activate
608
+ def test_update_named_credential_with_certificate(self):
609
+ """Test update of named credential with certificate"""
610
+ task = create_task(
611
+ UpdateNamedCredential,
612
+ {"name": "testNc", "parameters": [{"certificate": "MyCertificate"}]},
613
+ )
614
+
615
+ nc_id = "0XA1234567890ABC"
616
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
617
+
618
+ # Mock query for named credential ID
619
+ responses.add(
620
+ method="GET",
621
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+LIMIT+1",
622
+ json={"size": 1, "records": [{"Id": nc_id}]},
623
+ status=200,
624
+ )
625
+
626
+ # Mock get named credential object
627
+ responses.add(
628
+ method="GET",
629
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
630
+ json={
631
+ "Metadata": {
632
+ "namedCredentialType": "SecuredEndpoint",
633
+ "namedCredentialParameters": [
634
+ {
635
+ "parameterName": None,
636
+ "parameterType": "Url",
637
+ "parameterValue": "https://api.example.com",
638
+ }
639
+ ],
640
+ }
641
+ },
642
+ status=200,
643
+ )
644
+
645
+ # Mock update named credential
646
+ responses.add(
647
+ method="PATCH",
648
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
649
+ json={},
650
+ status=200,
651
+ )
652
+
653
+ task()
654
+ assert len(responses.calls) == 3
655
+
656
+ @responses.activate
657
+ def test_update_named_credential_with_transform_parameters(self):
658
+ """Test update of named credential with transform parameters from environment"""
659
+ with mock.patch.dict(
660
+ os.environ,
661
+ {
662
+ "TEST_URL": "https://env.example.com",
663
+ "TEST_AUTH": "EnvAuth",
664
+ "HEADER_VALUE": "env-header-value",
665
+ },
666
+ ):
667
+ task = create_task(
668
+ UpdateNamedCredential,
669
+ {
670
+ "name": "testNc",
671
+ "transform_parameters": [
672
+ {"url": "TEST_URL"},
673
+ {"authentication": "TEST_AUTH"},
674
+ {
675
+ "http_header": [
676
+ {
677
+ "name": "x-api-key",
678
+ "value": "HEADER_VALUE",
679
+ "secret": True,
680
+ }
681
+ ]
682
+ },
683
+ ],
684
+ },
685
+ )
686
+
687
+ nc_id = "0XA1234567890ABC"
688
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
689
+
690
+ # Mock query for named credential ID
691
+ responses.add(
692
+ method="GET",
693
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+LIMIT+1",
694
+ json={"size": 1, "records": [{"Id": nc_id}]},
695
+ status=200,
696
+ )
697
+
698
+ # Mock get named credential object
699
+ responses.add(
700
+ method="GET",
701
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
702
+ json={
703
+ "Metadata": {
704
+ "namedCredentialType": "SecuredEndpoint",
705
+ "namedCredentialParameters": [
706
+ {
707
+ "parameterName": None,
708
+ "parameterType": "Url",
709
+ "parameterValue": "https://old.example.com",
710
+ "certificate": None,
711
+ "description": None,
712
+ "externalCredential": None,
713
+ "sequenceNumber": None,
714
+ }
715
+ ],
716
+ }
717
+ },
718
+ status=200,
719
+ )
720
+
721
+ # Mock update named credential
722
+ responses.add(
723
+ method="PATCH",
724
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
725
+ json={},
726
+ status=200,
727
+ )
728
+
729
+ task()
730
+ assert len(responses.calls) == 3
731
+
732
+ @responses.activate
733
+ def test_update_named_credential_with_callout_options(self):
734
+ """Test update of named credential with callout options"""
735
+ task = create_task(
736
+ UpdateNamedCredential,
737
+ {
738
+ "name": "testNc",
739
+ "callout_options": {
740
+ "allow_merge_fields_in_body": True,
741
+ "allow_merge_fields_in_header": False,
742
+ "generate_authorization_header": True,
743
+ },
744
+ "parameters": [{"url": "https://api.example.com"}],
745
+ },
746
+ )
747
+
748
+ nc_id = "0XA1234567890ABC"
749
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
750
+
751
+ # Mock query for named credential ID
752
+ responses.add(
753
+ method="GET",
754
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+LIMIT+1",
755
+ json={"size": 1, "records": [{"Id": nc_id}]},
756
+ status=200,
757
+ )
758
+
759
+ # Mock get named credential object
760
+ responses.add(
761
+ method="GET",
762
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
763
+ json={
764
+ "Metadata": {
765
+ "namedCredentialType": "SecuredEndpoint",
766
+ "namedCredentialParameters": [
767
+ {
768
+ "parameterName": None,
769
+ "parameterType": "Url",
770
+ "parameterValue": "https://old.example.com",
771
+ }
772
+ ],
773
+ }
774
+ },
775
+ status=200,
776
+ )
777
+
778
+ # Mock update named credential
779
+ responses.add(
780
+ method="PATCH",
781
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
782
+ json={},
783
+ status=200,
784
+ )
785
+
786
+ task()
787
+ assert len(responses.calls) == 3
788
+
789
+ @responses.activate
790
+ def test_update_named_credential_no_template_parameter(self):
791
+ """Test update when no template parameter exists"""
792
+ task = create_task(
793
+ UpdateNamedCredential,
794
+ {"name": "testNc", "parameters": [{"url": "https://api.example.com"}]},
795
+ )
796
+
797
+ nc_id = "0XA1234567890ABC"
798
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
799
+
800
+ # Mock query for named credential ID
801
+ responses.add(
802
+ method="GET",
803
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+LIMIT+1",
804
+ json={"size": 1, "records": [{"Id": nc_id}]},
805
+ status=200,
806
+ )
807
+
808
+ # Mock get named credential object without Url parameter
809
+ responses.add(
810
+ method="GET",
811
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
812
+ json={
813
+ "Metadata": {
814
+ "namedCredentialType": "SecuredEndpoint",
815
+ "namedCredentialParameters": [],
816
+ }
817
+ },
818
+ status=200,
819
+ )
820
+
821
+ # Mock update named credential
822
+ responses.add(
823
+ method="PATCH",
824
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
825
+ json={},
826
+ status=200,
827
+ )
828
+
829
+ task()
830
+ assert len(responses.calls) == 3
831
+
832
+ @responses.activate
833
+ def test_update_named_credential_update_existing_parameter(self):
834
+ """Test updating an existing parameter"""
835
+ task = create_task(
836
+ UpdateNamedCredential,
837
+ {"name": "testNc", "parameters": [{"url": "https://new.example.com"}]},
838
+ )
839
+
840
+ nc_id = "0XA1234567890ABC"
841
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
842
+
843
+ # Mock query for named credential ID
844
+ responses.add(
845
+ method="GET",
846
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+LIMIT+1",
847
+ json={"size": 1, "records": [{"Id": nc_id}]},
848
+ status=200,
849
+ )
850
+
851
+ # Mock get named credential object with existing Url parameter
852
+ responses.add(
853
+ method="GET",
854
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
855
+ json={
856
+ "Metadata": {
857
+ "namedCredentialType": "SecuredEndpoint",
858
+ "namedCredentialParameters": [
859
+ {
860
+ "parameterName": None,
861
+ "parameterType": "Url",
862
+ "parameterValue": "https://old.example.com",
863
+ }
864
+ ],
865
+ }
866
+ },
867
+ status=200,
868
+ )
869
+
870
+ # Mock update named credential
871
+ responses.add(
872
+ method="PATCH",
873
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
874
+ json={},
875
+ status=200,
876
+ )
877
+
878
+ task()
879
+ assert len(responses.calls) == 3
880
+
881
+ @responses.activate
882
+ def test_update_named_credential_with_allowed_namespaces(self):
883
+ """Test update of named credential with allowed managed package namespaces"""
884
+ task = create_task(
885
+ UpdateNamedCredential,
886
+ {
887
+ "name": "testNc",
888
+ "parameters": [{"allowed_managed_package_namespaces": "th_dev"}],
889
+ },
890
+ )
891
+
892
+ nc_id = "0XA1234567890ABC"
893
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
894
+
895
+ # Mock query for named credential ID
896
+ responses.add(
897
+ method="GET",
898
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+LIMIT+1",
899
+ json={"size": 1, "records": [{"Id": nc_id}]},
900
+ status=200,
901
+ )
902
+
903
+ # Mock get named credential object
904
+ responses.add(
905
+ method="GET",
906
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
907
+ json={
908
+ "Metadata": {
909
+ "namedCredentialType": "SecuredEndpoint",
910
+ "namedCredentialParameters": [
911
+ {
912
+ "parameterName": None,
913
+ "parameterType": "Url",
914
+ "parameterValue": "https://api.example.com",
915
+ }
916
+ ],
917
+ }
918
+ },
919
+ status=200,
920
+ )
921
+
922
+ # Mock update named credential
923
+ responses.add(
924
+ method="PATCH",
925
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
926
+ json={},
927
+ status=200,
928
+ )
929
+
930
+ task()
931
+ assert len(responses.calls) == 3
932
+
933
+ @responses.activate
934
+ def test_update_named_credential_update_http_header_existing(self):
935
+ """Test updating existing HTTP header parameter"""
936
+ task = create_task(
937
+ UpdateNamedCredential,
938
+ {
939
+ "name": "testNc",
940
+ "parameters": [
941
+ {
942
+ "http_header": [
943
+ {"name": "x-api-key", "value": "new-value", "secret": True}
944
+ ]
945
+ }
946
+ ],
947
+ },
948
+ )
949
+
950
+ nc_id = "0XA1234567890ABC"
951
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
952
+
953
+ # Mock query for named credential ID
954
+ responses.add(
955
+ method="GET",
956
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+LIMIT+1",
957
+ json={"size": 1, "records": [{"Id": nc_id}]},
958
+ status=200,
959
+ )
960
+
961
+ # Mock get named credential object with existing HTTP header
962
+ responses.add(
963
+ method="GET",
964
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
965
+ json={
966
+ "Metadata": {
967
+ "namedCredentialType": "SecuredEndpoint",
968
+ "namedCredentialParameters": [
969
+ {
970
+ "parameterName": None,
971
+ "parameterType": "Url",
972
+ "parameterValue": "https://api.example.com",
973
+ },
974
+ {
975
+ "parameterName": "x-api-key",
976
+ "parameterType": "HttpHeader",
977
+ "parameterValue": "old-value",
978
+ },
979
+ ],
980
+ }
981
+ },
982
+ status=200,
983
+ )
984
+
985
+ # Mock update named credential
986
+ responses.add(
987
+ method="PATCH",
988
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
989
+ json={},
990
+ status=200,
991
+ )
992
+
993
+ task()
994
+ assert len(responses.calls) == 3
995
+
996
+ @responses.activate
997
+ def test_update_named_credential_exception_in_update(self):
998
+ """Test exception handling during update"""
999
+ task = create_task(
1000
+ UpdateNamedCredential,
1001
+ {"name": "testNc", "parameters": [{"url": "https://api.example.com"}]},
1002
+ )
1003
+
1004
+ nc_id = "0XA1234567890ABC"
1005
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
1006
+
1007
+ # Mock query for named credential ID
1008
+ responses.add(
1009
+ method="GET",
1010
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+NamedCredential+WHERE+DeveloperName%3D%27testNc%27+LIMIT+1",
1011
+ json={"size": 1, "records": [{"Id": nc_id}]},
1012
+ status=200,
1013
+ )
1014
+
1015
+ # Mock get named credential object
1016
+ responses.add(
1017
+ method="GET",
1018
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
1019
+ json={
1020
+ "Metadata": {
1021
+ "namedCredentialType": "SecuredEndpoint",
1022
+ "namedCredentialParameters": [
1023
+ {
1024
+ "parameterName": None,
1025
+ "parameterType": "Url",
1026
+ "parameterValue": "https://old.example.com",
1027
+ }
1028
+ ],
1029
+ }
1030
+ },
1031
+ status=200,
1032
+ )
1033
+
1034
+ # Mock update named credential - exception
1035
+ responses.add(
1036
+ method="PATCH",
1037
+ url=f"{tooling_url}/sobjects/NamedCredential/{nc_id}",
1038
+ body=Exception("Connection error"),
1039
+ )
1040
+
1041
+ with pytest.raises(Exception): # Can be TypeError or SalesforceDXException
1042
+ task()