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,1427 @@
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_external_credential import (
9
+ ExternalCredential,
10
+ ExternalCredentialParameter,
11
+ ExtParameter,
12
+ HttpHeader,
13
+ TransformExternalCredentialParameter,
14
+ UpdateExternalCredential,
15
+ )
16
+ from cumulusci.tests.util import CURRENT_SF_API_VERSION
17
+
18
+ from .util import create_task
19
+
20
+
21
+ class TestExtParameter:
22
+ """Test ExtParameter model"""
23
+
24
+ def test_ext_parameter_defaults(self):
25
+ """Test default values for ext parameter"""
26
+ param = ExtParameter(name="test-param", value="test-value")
27
+ assert param.name == "test-param"
28
+ assert param.value == "test-value"
29
+ assert param.group is None
30
+ assert param.sequence_number is None
31
+
32
+ def test_ext_parameter_with_all_fields(self):
33
+ """Test ext parameter with all fields"""
34
+ param = ExtParameter(
35
+ name="test-param", value="test-value", group="TestGroup", sequence_number=1
36
+ )
37
+ assert param.name == "test-param"
38
+ assert param.value == "test-value"
39
+ assert param.group == "TestGroup"
40
+ assert param.sequence_number == 1
41
+
42
+
43
+ class TestHttpHeader:
44
+ """Test HttpHeader model"""
45
+
46
+ def test_http_header_defaults(self):
47
+ """Test default values for http header"""
48
+ header = HttpHeader(name="test-header", value="test-value")
49
+ assert header.name == "test-header"
50
+ assert header.value == "test-value"
51
+ assert header.secret is False
52
+ assert header.sequence_number is None
53
+
54
+ def test_http_header_with_secret(self):
55
+ """Test http header with secret flag"""
56
+ header = HttpHeader(
57
+ name="api-key", value="secret123", secret=True, sequence_number=1
58
+ )
59
+ assert header.name == "api-key"
60
+ assert header.value == "secret123"
61
+ assert header.secret is True
62
+ assert header.sequence_number == 1
63
+
64
+
65
+ class TestExternalCredential:
66
+ """Test ExternalCredential model"""
67
+
68
+ def test_external_credential_defaults(self):
69
+ """Test default values for external credential"""
70
+ cred = ExternalCredential(name="test-cred", value="test-value")
71
+ assert cred.name == "test-cred"
72
+ assert cred.value == "test-value"
73
+ assert cred.client_secret is None
74
+ assert cred.client_id is None
75
+ assert cred.auth_protocol == "OAuth"
76
+
77
+ def test_external_credential_with_oauth(self):
78
+ """Test external credential with OAuth fields"""
79
+ cred = ExternalCredential(
80
+ name="oauth-cred",
81
+ value="test-value",
82
+ client_id="client123",
83
+ client_secret="secret456",
84
+ auth_protocol="OAuth2",
85
+ )
86
+ assert cred.name == "oauth-cred"
87
+ assert cred.value == "test-value"
88
+ assert cred.client_id == "client123"
89
+ assert cred.client_secret == "secret456"
90
+ assert cred.auth_protocol == "OAuth2"
91
+
92
+
93
+ class TestExternalCredentialParameter:
94
+ """Test ExternalCredentialParameter model"""
95
+
96
+ def test_parameter_with_auth_header(self):
97
+ """Test parameter with auth header"""
98
+ auth_header = HttpHeader(name="Authorization", value="Bearer token123")
99
+ param = ExternalCredentialParameter(auth_header=auth_header)
100
+ assert param.auth_header == auth_header
101
+ result = param.get_external_credential_parameter()
102
+ assert result["parameterType"] == "AuthHeader"
103
+ assert result["parameterValue"] == "Bearer token123"
104
+ assert result["parameterName"] == "Authorization"
105
+
106
+ def test_parameter_with_auth_provider(self):
107
+ """Test parameter with auth provider"""
108
+ param = ExternalCredentialParameter(auth_provider="MyAuthProvider")
109
+ assert param.auth_provider == "MyAuthProvider"
110
+ result = param.get_external_credential_parameter()
111
+ assert result["parameterType"] == "AuthProvider"
112
+ assert result["authProvider"] == "MyAuthProvider"
113
+ assert result["parameterName"] == "AuthProvider"
114
+
115
+ def test_parameter_with_auth_provider_url(self):
116
+ """Test parameter with auth provider URL"""
117
+ param = ExternalCredentialParameter(
118
+ auth_provider_url="https://auth.example.com"
119
+ )
120
+ assert param.auth_provider_url == "https://auth.example.com"
121
+ result = param.get_external_credential_parameter()
122
+ assert result["parameterType"] == "AuthProviderUrl"
123
+ assert result["parameterValue"] == "https://auth.example.com"
124
+
125
+ def test_parameter_with_jwt_body_claim(self):
126
+ """Test parameter with JWT body claim"""
127
+ jwt_claim = ExtParameter(name="sub", value='{"sub":"user123"}')
128
+ param = ExternalCredentialParameter(jwt_body_claim=jwt_claim)
129
+ assert param.jwt_body_claim == jwt_claim
130
+ result = param.get_external_credential_parameter()
131
+ assert result["parameterType"] == "JwtBodyClaim"
132
+ assert result["parameterValue"] == '{"sub":"user123"}'
133
+ assert result["parameterName"] == "sub"
134
+
135
+ def test_parameter_with_named_principal(self):
136
+ """Test parameter with named principal"""
137
+ named_principal = ExternalCredential(
138
+ name="MyPrincipal",
139
+ value="test-value",
140
+ client_id="client123",
141
+ client_secret="secret456",
142
+ )
143
+ param = ExternalCredentialParameter(named_principal=named_principal)
144
+ assert param.named_principal == named_principal
145
+ result = param.get_external_credential_parameter()
146
+ assert result["parameterType"] == "NamedPrincipal"
147
+ assert result["parameterName"] == "MyPrincipal"
148
+
149
+ def test_parameter_with_signing_certificate(self):
150
+ """Test parameter with signing certificate"""
151
+ param = ExternalCredentialParameter(signing_certificate="MyCertificate")
152
+ assert param.signing_certificate == "MyCertificate"
153
+ result = param.get_external_credential_parameter()
154
+ assert result["parameterType"] == "SigningCertificate"
155
+ assert result["parameterValue"] == "MyCertificate"
156
+ assert result["parameterName"] == "SigningCertificate"
157
+
158
+ def test_parameter_validation_error_no_params(self):
159
+ """Test that at least one parameter must be provided"""
160
+ with pytest.raises(
161
+ ValueError, match="At least and only one parameter must be provided"
162
+ ):
163
+ ExternalCredentialParameter()
164
+
165
+ def test_parameter_validation_error_multiple_params(self):
166
+ """Test that only one parameter can be provided"""
167
+ auth_header = HttpHeader(name="Auth", value="Bearer token")
168
+ with pytest.raises(
169
+ ValueError, match="At least and only one parameter must be provided"
170
+ ):
171
+ ExternalCredentialParameter(
172
+ auth_header=auth_header, auth_provider="MyProvider"
173
+ )
174
+
175
+ def test_get_principal_credential(self):
176
+ """Test get_principal_credential method"""
177
+ named_principal = ExternalCredential(name="MyPrincipal", value="test")
178
+ param = ExternalCredentialParameter(named_principal=named_principal)
179
+ result = param.get_principal_credential("MyExternalCredential")
180
+ assert result["principalType"] == "NamedPrincipal"
181
+ assert result["principalName"] == "MyPrincipal"
182
+ assert result["externalCredential"] == "MyExternalCredential"
183
+
184
+ def test_get_credential_parameter(self):
185
+ """Test get_credential_parameter method"""
186
+ named_principal = ExternalCredential(
187
+ name="MyPrincipal",
188
+ value="test",
189
+ client_id="client123",
190
+ client_secret="secret456",
191
+ )
192
+ param = ExternalCredentialParameter(named_principal=named_principal)
193
+ result = param.get_credential_parameter()
194
+ assert result["clientId"]["value"] == "client123"
195
+ assert result["clientSecret"]["value"] == "secret456"
196
+ assert result["clientId"]["encrypted"] is False
197
+ assert result["clientSecret"]["encrypted"] is True
198
+
199
+ def test_get_credential(self):
200
+ """Test get_credential method"""
201
+ named_principal = ExternalCredential(
202
+ name="MyPrincipal",
203
+ value="test",
204
+ client_id="client123",
205
+ client_secret="secret456",
206
+ auth_protocol="OAuth2",
207
+ )
208
+ param = ExternalCredentialParameter(named_principal=named_principal)
209
+ result = param.get_credential("MyExternalCredential")
210
+ assert result["principalType"] == "NamedPrincipal"
211
+ assert result["principalName"] == "MyPrincipal"
212
+ assert result["externalCredential"] == "MyExternalCredential"
213
+ assert result["authenticationProtocol"] == "OAuth2"
214
+
215
+
216
+ class TestTransformExternalCredentialParameter:
217
+ """Test TransformExternalCredentialParameter model"""
218
+
219
+ def test_transform_parameter_from_env(self):
220
+ """Test parameter transformation from environment variable"""
221
+ with mock.patch.dict(os.environ, {"MY_AUTH_HEADER": "Bearer token456"}):
222
+ auth_header = HttpHeader(name="Authorization", value="MY_AUTH_HEADER")
223
+ param = TransformExternalCredentialParameter(auth_header=auth_header)
224
+ result = param.get_external_credential_parameter()
225
+ assert result["parameterValue"] == "Bearer token456"
226
+
227
+ def test_transform_parameter_auth_provider_from_env(self):
228
+ """Test auth provider transformation from environment variable"""
229
+ with mock.patch.dict(os.environ, {"MY_AUTH_PROVIDER": "EnvAuthProvider"}):
230
+ param = TransformExternalCredentialParameter(
231
+ auth_provider="MY_AUTH_PROVIDER"
232
+ )
233
+ result = param.get_external_credential_parameter()
234
+ assert (
235
+ result["authProvider"] == "MY_AUTH_PROVIDER"
236
+ ) # Transform doesn't apply to authProvider field
237
+
238
+ def test_transform_parameter_missing_env(self):
239
+ """Test parameter transformation with missing environment variable"""
240
+ auth_header = HttpHeader(name="Auth", value="NONEXISTENT_ENV_VAR")
241
+ param = TransformExternalCredentialParameter(auth_header=auth_header)
242
+ result = param.get_external_credential_parameter()
243
+ assert result["parameterValue"] is None
244
+
245
+ def test_transform_credential_parameter_from_env(self):
246
+ """Test credential parameter transformation from environment variable"""
247
+ with mock.patch.dict(os.environ, {"CLIENT_SECRET": "secret123"}):
248
+ named_principal = ExternalCredential(
249
+ name="MyPrincipal",
250
+ value="test",
251
+ client_id="client123",
252
+ client_secret="CLIENT_SECRET",
253
+ )
254
+ param = TransformExternalCredentialParameter(
255
+ named_principal=named_principal
256
+ )
257
+ result = param.get_credential_parameter()
258
+ assert result["clientSecret"]["value"] == "secret123"
259
+
260
+
261
+ class TestUpdateExternalCredential:
262
+ """Test UpdateExternalCredential task"""
263
+
264
+ @responses.activate
265
+ def test_update_external_credential_success(self):
266
+ """Test successful update of external credential"""
267
+ auth_header = HttpHeader(name="Authorization", value="Bearer newtoken123")
268
+ task = create_task(
269
+ UpdateExternalCredential,
270
+ {
271
+ "name": "testExtCred",
272
+ "namespace": "",
273
+ "parameters": [{"auth_header": auth_header}],
274
+ },
275
+ )
276
+
277
+ ext_cred_id = "0XE1234567890ABC"
278
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
279
+
280
+ # Mock query for external credential ID
281
+ responses.add(
282
+ method="GET",
283
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
284
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
285
+ status=200,
286
+ )
287
+
288
+ # Mock get external credential object
289
+ responses.add(
290
+ method="GET",
291
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
292
+ json={
293
+ "Metadata": {
294
+ "description": "Old description",
295
+ "externalCredentialParameters": [
296
+ {
297
+ "parameterType": "AuthHeader",
298
+ "parameterValue": "Bearer oldtoken123",
299
+ "description": None,
300
+ "parameterName": None,
301
+ "sequenceNumber": None,
302
+ }
303
+ ],
304
+ }
305
+ },
306
+ status=200,
307
+ )
308
+
309
+ # Mock update external credential
310
+ responses.add(
311
+ method="PATCH",
312
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
313
+ json={},
314
+ status=200,
315
+ )
316
+
317
+ task()
318
+ assert len(responses.calls) == 3
319
+
320
+ @responses.activate
321
+ def test_update_external_credential_with_named_principal(self):
322
+ """Test update with named principal and credential management"""
323
+ named_principal = ExternalCredential(
324
+ name="MyPrincipal",
325
+ value="test-value",
326
+ client_id="client123",
327
+ client_secret="secret456",
328
+ auth_protocol="OAuth2",
329
+ )
330
+ task = create_task(
331
+ UpdateExternalCredential,
332
+ {
333
+ "name": "testExtCred",
334
+ "parameters": [{"named_principal": named_principal}],
335
+ },
336
+ )
337
+
338
+ ext_cred_id = "0XE1234567890ABC"
339
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
340
+ connect_url = (
341
+ f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}"
342
+ )
343
+
344
+ # Mock query for external credential ID
345
+ responses.add(
346
+ method="GET",
347
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
348
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
349
+ status=200,
350
+ )
351
+
352
+ # Mock get external credential object
353
+ responses.add(
354
+ method="GET",
355
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
356
+ json={
357
+ "Metadata": {
358
+ "externalCredentialParameters": [
359
+ {
360
+ "parameterType": "NamedPrincipal",
361
+ "parameterName": "MyPrincipal",
362
+ "parameterValue": None,
363
+ }
364
+ ],
365
+ }
366
+ },
367
+ status=200,
368
+ )
369
+
370
+ # Mock update external credential
371
+ responses.add(
372
+ method="PATCH",
373
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
374
+ json={},
375
+ status=200,
376
+ )
377
+
378
+ # Mock get credential (existing)
379
+ responses.add(
380
+ method="GET",
381
+ url=f"{connect_url}/named-credentials/credential",
382
+ json={
383
+ "principalType": "NamedPrincipal",
384
+ "principalName": "MyPrincipal",
385
+ "externalCredential": "testExtCred",
386
+ "authenticationStatus": "Configured",
387
+ "credentials": {
388
+ "clientId": {
389
+ "value": "client123",
390
+ "encrypted": False,
391
+ },
392
+ },
393
+ },
394
+ status=200,
395
+ )
396
+
397
+ # Mock update credential
398
+ responses.add(
399
+ method="PUT",
400
+ url=f"{connect_url}/named-credentials/credential",
401
+ json={},
402
+ status=200,
403
+ )
404
+
405
+ task()
406
+ assert len(responses.calls) == 5
407
+
408
+ @responses.activate
409
+ def test_update_external_credential_create_new_credential(self):
410
+ """Test creating new credential when it doesn't exist"""
411
+ named_principal = ExternalCredential(
412
+ name="NewPrincipal",
413
+ value="test-value",
414
+ client_id="client123",
415
+ client_secret="secret456",
416
+ )
417
+ task = create_task(
418
+ UpdateExternalCredential,
419
+ {
420
+ "name": "testExtCred",
421
+ "parameters": [{"named_principal": named_principal}],
422
+ },
423
+ )
424
+
425
+ ext_cred_id = "0XE1234567890ABC"
426
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
427
+ connect_url = (
428
+ f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}"
429
+ )
430
+
431
+ # Mock query for external credential ID
432
+ responses.add(
433
+ method="GET",
434
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
435
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
436
+ status=200,
437
+ )
438
+
439
+ # Mock get external credential object
440
+ responses.add(
441
+ method="GET",
442
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
443
+ json={
444
+ "Metadata": {
445
+ "externalCredentialParameters": [],
446
+ }
447
+ },
448
+ status=200,
449
+ )
450
+
451
+ # Mock update external credential
452
+ responses.add(
453
+ method="PATCH",
454
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
455
+ json={},
456
+ status=200,
457
+ )
458
+
459
+ # Mock get credential (not found)
460
+ responses.add(
461
+ method="GET",
462
+ url=f"{connect_url}/named-credentials/credential",
463
+ json={
464
+ "principalType": "NamedPrincipal",
465
+ "principalName": "MyPrincipal",
466
+ "externalCredential": "testExtCred",
467
+ "authenticationStatus": "Configured",
468
+ "credentials": {},
469
+ },
470
+ status=200,
471
+ )
472
+
473
+ # Mock create credential
474
+ responses.add(
475
+ method="POST",
476
+ url=f"{connect_url}/named-credentials/credential",
477
+ json={},
478
+ status=201,
479
+ )
480
+
481
+ task()
482
+ assert len(responses.calls) == 5
483
+
484
+ @responses.activate
485
+ def test_update_external_credential_not_found(self):
486
+ """Test update of non-existent external credential"""
487
+ auth_header = HttpHeader(name="Authorization", value="Bearer token")
488
+ task = create_task(
489
+ UpdateExternalCredential,
490
+ {"name": "nonExistentCred", "parameters": [{"auth_header": auth_header}]},
491
+ )
492
+
493
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
494
+
495
+ # Mock query returning no results
496
+ responses.add(
497
+ method="GET",
498
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27nonExistentCred%27+LIMIT+1",
499
+ json={"size": 0, "records": []},
500
+ status=200,
501
+ )
502
+
503
+ with pytest.raises(
504
+ SalesforceDXException,
505
+ match="External credential 'nonExistentCred' not found",
506
+ ):
507
+ task()
508
+
509
+ @responses.activate
510
+ def test_update_external_credential_with_namespace(self):
511
+ """Test update of external credential with namespace"""
512
+ # Mock SF API version discovery
513
+ responses.add(
514
+ method="GET",
515
+ url="https://test.salesforce.com/services/data",
516
+ json=[{"version": CURRENT_SF_API_VERSION}],
517
+ status=200,
518
+ )
519
+
520
+ # Mock installed packages query
521
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
522
+ responses.add(
523
+ method="GET",
524
+ url=f"{tooling_url}/query/?q=SELECT+SubscriberPackage.Id%2C+SubscriberPackage.Name%2C+SubscriberPackage.NamespacePrefix%2C+SubscriberPackageVersionId+FROM+InstalledSubscriberPackage",
525
+ json={"size": 0, "records": []},
526
+ status=200,
527
+ )
528
+
529
+ task = create_task(
530
+ UpdateExternalCredential,
531
+ {
532
+ "name": "testExtCred",
533
+ "namespace": "myns",
534
+ "parameters": [{"auth_provider": "MyAuthProvider"}],
535
+ },
536
+ )
537
+
538
+ ext_cred_id = "0XE1234567890ABC"
539
+
540
+ # Mock query for external credential ID
541
+ responses.add(
542
+ method="GET",
543
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+AND+NamespacePrefix%3D%27myns%27+LIMIT+1",
544
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
545
+ status=200,
546
+ )
547
+
548
+ # Mock get external credential object
549
+ responses.add(
550
+ method="GET",
551
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
552
+ json={
553
+ "Metadata": {
554
+ "externalCredentialParameters": [
555
+ {
556
+ "parameterType": "AuthProvider",
557
+ "parameterValue": "OldAuthProvider",
558
+ "description": None,
559
+ "parameterName": None,
560
+ "sequenceNumber": None,
561
+ }
562
+ ],
563
+ }
564
+ },
565
+ status=200,
566
+ )
567
+
568
+ # Mock update external credential
569
+ responses.add(
570
+ method="PATCH",
571
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
572
+ json={},
573
+ status=200,
574
+ )
575
+
576
+ task()
577
+ assert len(responses.calls) == 5 # 2 setup + 3 task calls
578
+
579
+ @responses.activate
580
+ def test_update_external_credential_add_new_parameter(self):
581
+ """Test adding a new parameter to external credential"""
582
+ jwt_claim = ExtParameter(name="sub", value='{"sub":"user123"}')
583
+ task = create_task(
584
+ UpdateExternalCredential,
585
+ {
586
+ "name": "testExtCred",
587
+ "parameters": [{"jwt_body_claim": jwt_claim}],
588
+ },
589
+ )
590
+
591
+ ext_cred_id = "0XE1234567890ABC"
592
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
593
+
594
+ # Mock query for external credential ID
595
+ responses.add(
596
+ method="GET",
597
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
598
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
599
+ status=200,
600
+ )
601
+
602
+ # Mock get external credential object with existing parameter
603
+ responses.add(
604
+ method="GET",
605
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
606
+ json={
607
+ "Metadata": {
608
+ "externalCredentialParameters": [
609
+ {
610
+ "parameterType": "AuthHeader",
611
+ "parameterValue": "Bearer token123",
612
+ "description": None,
613
+ "parameterName": None,
614
+ "sequenceNumber": None,
615
+ }
616
+ ],
617
+ }
618
+ },
619
+ status=200,
620
+ )
621
+
622
+ # Mock update external credential
623
+ responses.add(
624
+ method="PATCH",
625
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
626
+ json={},
627
+ status=200,
628
+ )
629
+
630
+ task()
631
+ assert len(responses.calls) == 3
632
+
633
+ @responses.activate
634
+ def test_update_external_credential_with_transform_parameters(self):
635
+ """Test update with transform parameters from environment variables"""
636
+ with mock.patch.dict(os.environ, {"MY_AUTH_TOKEN": "Bearer envtoken789"}):
637
+ auth_header = HttpHeader(name="Authorization", value="MY_AUTH_TOKEN")
638
+ task = create_task(
639
+ UpdateExternalCredential,
640
+ {
641
+ "name": "testExtCred",
642
+ "transform_parameters": [{"auth_header": auth_header}],
643
+ },
644
+ )
645
+
646
+ ext_cred_id = "0XE1234567890ABC"
647
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
648
+
649
+ # Mock query for external credential ID
650
+ responses.add(
651
+ method="GET",
652
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
653
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
654
+ status=200,
655
+ )
656
+
657
+ # Mock get external credential object
658
+ responses.add(
659
+ method="GET",
660
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
661
+ json={
662
+ "Metadata": {
663
+ "externalCredentialParameters": [
664
+ {
665
+ "parameterType": "AuthHeader",
666
+ "parameterValue": "Bearer oldtoken",
667
+ "description": None,
668
+ "parameterName": None,
669
+ "sequenceNumber": None,
670
+ }
671
+ ],
672
+ }
673
+ },
674
+ status=200,
675
+ )
676
+
677
+ # Mock update external credential
678
+ responses.add(
679
+ method="PATCH",
680
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
681
+ json={},
682
+ status=200,
683
+ )
684
+
685
+ task()
686
+ assert len(responses.calls) == 3
687
+
688
+ @responses.activate
689
+ def test_update_external_credential_retrieve_error(self):
690
+ """Test error handling when retrieving external credential fails"""
691
+ auth_header = HttpHeader(name="Authorization", value="Bearer token")
692
+ task = create_task(
693
+ UpdateExternalCredential,
694
+ {"name": "testExtCred", "parameters": [{"auth_header": auth_header}]},
695
+ )
696
+
697
+ ext_cred_id = "0XE1234567890ABC"
698
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
699
+
700
+ # Mock query for external credential ID
701
+ responses.add(
702
+ method="GET",
703
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
704
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
705
+ status=200,
706
+ )
707
+
708
+ # Mock get external credential object failure
709
+ responses.add(
710
+ method="GET",
711
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
712
+ json={"error": "Not Found"},
713
+ status=404,
714
+ )
715
+
716
+ with pytest.raises(
717
+ SalesforceDXException,
718
+ match="Failed to retrieve external credential object for 'testExtCred'",
719
+ ):
720
+ task()
721
+
722
+ @responses.activate
723
+ def test_update_external_credential_update_error(self):
724
+ """Test error handling when updating external credential fails"""
725
+ auth_header = HttpHeader(name="Authorization", value="Bearer token")
726
+ task = create_task(
727
+ UpdateExternalCredential,
728
+ {"name": "testExtCred", "parameters": [{"auth_header": auth_header}]},
729
+ )
730
+
731
+ ext_cred_id = "0XE1234567890ABC"
732
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
733
+
734
+ # Mock query for external credential ID
735
+ responses.add(
736
+ method="GET",
737
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
738
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
739
+ status=200,
740
+ )
741
+
742
+ # Mock get external credential object
743
+ responses.add(
744
+ method="GET",
745
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
746
+ json={
747
+ "Metadata": {
748
+ "externalCredentialParameters": [
749
+ {
750
+ "parameterType": "AuthHeader",
751
+ "parameterValue": "Bearer oldtoken",
752
+ "description": None,
753
+ "parameterName": None,
754
+ "sequenceNumber": None,
755
+ }
756
+ ],
757
+ }
758
+ },
759
+ status=200,
760
+ )
761
+
762
+ # Mock update external credential failure
763
+ responses.add(
764
+ method="PATCH",
765
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
766
+ json={"error": "Update failed"},
767
+ status=400,
768
+ )
769
+
770
+ with pytest.raises(
771
+ SalesforceDXException,
772
+ match="Failed to update external credential object",
773
+ ):
774
+ task()
775
+
776
+ @responses.activate
777
+ def test_update_external_credential_no_existing_parameters(self):
778
+ """Test update when external credential has no existing parameters"""
779
+ # Mock SF API version discovery
780
+ responses.add(
781
+ method="GET",
782
+ url="https://test.salesforce.com/services/data",
783
+ json=[{"version": CURRENT_SF_API_VERSION}],
784
+ status=200,
785
+ )
786
+
787
+ # Mock installed packages query
788
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
789
+ responses.add(
790
+ method="GET",
791
+ url=f"{tooling_url}/query/?q=SELECT+SubscriberPackage.Id%2C+SubscriberPackage.Name%2C+SubscriberPackage.NamespacePrefix%2C+SubscriberPackageVersionId+FROM+InstalledSubscriberPackage",
792
+ json={"size": 0, "records": []},
793
+ status=200,
794
+ )
795
+
796
+ task = create_task(
797
+ UpdateExternalCredential,
798
+ {
799
+ "name": "testExtCred",
800
+ "parameters": [{"auth_provider": "NewAuthProvider"}],
801
+ },
802
+ )
803
+
804
+ ext_cred_id = "0XE1234567890ABC"
805
+
806
+ # Mock query for external credential ID
807
+ responses.add(
808
+ method="GET",
809
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
810
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
811
+ status=200,
812
+ )
813
+
814
+ # Mock get external credential object with no parameters
815
+ responses.add(
816
+ method="GET",
817
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
818
+ json={"Metadata": {"externalCredentialParameters": []}},
819
+ status=200,
820
+ )
821
+
822
+ # Mock update external credential
823
+ responses.add(
824
+ method="PATCH",
825
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
826
+ json={},
827
+ status=200,
828
+ )
829
+
830
+ task()
831
+ assert len(responses.calls) == 5 # 2 setup + 3 task calls
832
+
833
+ @responses.activate
834
+ def test_update_external_credential_with_multiple_parameters(self):
835
+ """Test update with multiple parameters"""
836
+ # Mock SF API version discovery
837
+ responses.add(
838
+ method="GET",
839
+ url="https://test.salesforce.com/services/data",
840
+ json=[{"version": CURRENT_SF_API_VERSION}],
841
+ status=200,
842
+ )
843
+
844
+ # Mock installed packages query
845
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
846
+ responses.add(
847
+ method="GET",
848
+ url=f"{tooling_url}/query/?q=SELECT+SubscriberPackage.Id%2C+SubscriberPackage.Name%2C+SubscriberPackage.NamespacePrefix%2C+SubscriberPackageVersionId+FROM+InstalledSubscriberPackage",
849
+ json={"size": 0, "records": []},
850
+ status=200,
851
+ )
852
+
853
+ auth_header = HttpHeader(name="Authorization", value="Bearer token123")
854
+ jwt_claim = ExtParameter(name="sub", value='{"sub":"user"}')
855
+ task = create_task(
856
+ UpdateExternalCredential,
857
+ {
858
+ "name": "testExtCred",
859
+ "parameters": [
860
+ {"auth_header": auth_header},
861
+ {"auth_provider": "MyAuthProvider"},
862
+ {"jwt_body_claim": jwt_claim},
863
+ ],
864
+ },
865
+ )
866
+
867
+ ext_cred_id = "0XE1234567890ABC"
868
+
869
+ # Mock query for external credential ID
870
+ responses.add(
871
+ method="GET",
872
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
873
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
874
+ status=200,
875
+ )
876
+
877
+ # Mock get external credential object
878
+ responses.add(
879
+ method="GET",
880
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
881
+ json={
882
+ "Metadata": {
883
+ "externalCredentialParameters": [
884
+ {
885
+ "parameterType": "AuthHeader",
886
+ "parameterValue": "Bearer oldtoken",
887
+ "description": None,
888
+ "parameterName": None,
889
+ "sequenceNumber": None,
890
+ }
891
+ ],
892
+ }
893
+ },
894
+ status=200,
895
+ )
896
+
897
+ # Mock update external credential
898
+ responses.add(
899
+ method="PATCH",
900
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
901
+ json={},
902
+ status=200,
903
+ )
904
+
905
+ task()
906
+ assert len(responses.calls) == 5 # 2 setup + 3 task calls
907
+
908
+ @responses.activate
909
+ def test_update_external_credential_with_sequence_number(self):
910
+ """Test update with sequence number"""
911
+ auth_header = HttpHeader(
912
+ name="MyHeader", value="Bearer token123", sequence_number=5
913
+ )
914
+ task = create_task(
915
+ UpdateExternalCredential,
916
+ {
917
+ "name": "testExtCred",
918
+ "parameters": [{"auth_header": auth_header}],
919
+ },
920
+ )
921
+
922
+ ext_cred_id = "0XE1234567890ABC"
923
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
924
+
925
+ # Mock query for external credential ID
926
+ responses.add(
927
+ method="GET",
928
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
929
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
930
+ status=200,
931
+ )
932
+
933
+ # Mock get external credential object
934
+ responses.add(
935
+ method="GET",
936
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
937
+ json={
938
+ "Metadata": {
939
+ "externalCredentialParameters": [
940
+ {
941
+ "parameterType": "AuthHeader",
942
+ "parameterValue": "Bearer oldtoken",
943
+ "description": None,
944
+ "parameterName": None,
945
+ "sequenceNumber": None,
946
+ }
947
+ ],
948
+ }
949
+ },
950
+ status=200,
951
+ )
952
+
953
+ # Mock update external credential
954
+ responses.add(
955
+ method="PATCH",
956
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
957
+ json={},
958
+ status=200,
959
+ )
960
+
961
+ task()
962
+ assert len(responses.calls) == 3
963
+
964
+ @responses.activate
965
+ def test_auth_provider_removes_external_auth_identity_provider(self):
966
+ """Test that adding AuthProvider removes ExternalAuthIdentityProvider"""
967
+ # Mock SF API version discovery
968
+ responses.add(
969
+ method="GET",
970
+ url="https://test.salesforce.com/services/data",
971
+ json=[{"version": CURRENT_SF_API_VERSION}],
972
+ status=200,
973
+ )
974
+
975
+ # Mock installed packages query
976
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
977
+ responses.add(
978
+ method="GET",
979
+ url=f"{tooling_url}/query/?q=SELECT+SubscriberPackage.Id%2C+SubscriberPackage.Name%2C+SubscriberPackage.NamespacePrefix%2C+SubscriberPackageVersionId+FROM+InstalledSubscriberPackage",
980
+ json={"size": 0, "records": []},
981
+ status=200,
982
+ )
983
+
984
+ task = create_task(
985
+ UpdateExternalCredential,
986
+ {
987
+ "name": "testExtCred",
988
+ "parameters": [{"auth_provider": "MyAuthProvider"}],
989
+ },
990
+ )
991
+
992
+ ext_cred_id = "0XE1234567890ABC"
993
+
994
+ # Mock query for external credential ID
995
+ responses.add(
996
+ method="GET",
997
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
998
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
999
+ status=200,
1000
+ )
1001
+
1002
+ # Mock get external credential object with ExternalAuthIdentityProvider
1003
+ responses.add(
1004
+ method="GET",
1005
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
1006
+ json={
1007
+ "Metadata": {
1008
+ "externalCredentialParameters": [
1009
+ {
1010
+ "parameterType": "ExternalAuthIdentityProvider",
1011
+ "parameterName": "ExternalAuthIdentityProvider",
1012
+ "externalAuthIdentityProvider": "OldProvider",
1013
+ "authProvider": None,
1014
+ "certificate": None,
1015
+ "description": None,
1016
+ "parameterGroup": "DefaultGroup",
1017
+ "parameterValue": None,
1018
+ "sequenceNumber": None,
1019
+ },
1020
+ {
1021
+ "parameterType": "AuthParameter",
1022
+ "parameterName": "Scope",
1023
+ "parameterValue": "scope",
1024
+ "authProvider": None,
1025
+ "certificate": None,
1026
+ "description": None,
1027
+ "externalAuthIdentityProvider": None,
1028
+ "parameterGroup": "DefaultGroup",
1029
+ "sequenceNumber": None,
1030
+ },
1031
+ ],
1032
+ }
1033
+ },
1034
+ status=200,
1035
+ )
1036
+
1037
+ # Mock update external credential
1038
+ responses.add(
1039
+ method="PATCH",
1040
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
1041
+ json={},
1042
+ status=200,
1043
+ )
1044
+
1045
+ task()
1046
+ assert len(responses.calls) == 5 # 2 setup + 3 task calls
1047
+
1048
+ # Verify the PATCH request removed ExternalAuthIdentityProvider
1049
+ patch_call = responses.calls[4] # Last call is the PATCH
1050
+ updated_params = patch_call.request.body
1051
+ import json
1052
+
1053
+ body = json.loads(updated_params)
1054
+ params = body["Metadata"]["externalCredentialParameters"]
1055
+
1056
+ # Should not contain ExternalAuthIdentityProvider
1057
+ assert not any(
1058
+ p.get("parameterType") == "ExternalAuthIdentityProvider" for p in params
1059
+ )
1060
+ # Should contain AuthProvider
1061
+ assert any(p.get("parameterType") == "AuthProvider" for p in params)
1062
+ # Should still contain AuthParameter
1063
+ assert any(p.get("parameterType") == "AuthParameter" for p in params)
1064
+
1065
+ @responses.activate
1066
+ def test_external_auth_identity_provider_removes_auth_provider(self):
1067
+ """Test that adding ExternalAuthIdentityProvider removes AuthProvider"""
1068
+ # Mock SF API version discovery
1069
+ responses.add(
1070
+ method="GET",
1071
+ url="https://test.salesforce.com/services/data",
1072
+ json=[{"version": CURRENT_SF_API_VERSION}],
1073
+ status=200,
1074
+ )
1075
+
1076
+ # Mock installed packages query
1077
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
1078
+ responses.add(
1079
+ method="GET",
1080
+ url=f"{tooling_url}/query/?q=SELECT+SubscriberPackage.Id%2C+SubscriberPackage.Name%2C+SubscriberPackage.NamespacePrefix%2C+SubscriberPackageVersionId+FROM+InstalledSubscriberPackage",
1081
+ json={"size": 0, "records": []},
1082
+ status=200,
1083
+ )
1084
+
1085
+ task = create_task(
1086
+ UpdateExternalCredential,
1087
+ {
1088
+ "name": "testExtCred",
1089
+ "parameters": [
1090
+ {"external_auth_identity_provider": "MyExternalAuthProvider"}
1091
+ ],
1092
+ },
1093
+ )
1094
+
1095
+ ext_cred_id = "0XE1234567890ABC"
1096
+
1097
+ # Mock query for external credential ID
1098
+ responses.add(
1099
+ method="GET",
1100
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
1101
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
1102
+ status=200,
1103
+ )
1104
+
1105
+ # Mock get external credential object with AuthProvider
1106
+ responses.add(
1107
+ method="GET",
1108
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
1109
+ json={
1110
+ "Metadata": {
1111
+ "externalCredentialParameters": [
1112
+ {
1113
+ "parameterType": "AuthProvider",
1114
+ "parameterName": "AuthProvider",
1115
+ "authProvider": "OldAuthProvider",
1116
+ "certificate": None,
1117
+ "description": None,
1118
+ "externalAuthIdentityProvider": None,
1119
+ "parameterGroup": "DefaultGroup",
1120
+ "parameterValue": "OldAuthProvider",
1121
+ "sequenceNumber": None,
1122
+ },
1123
+ {
1124
+ "parameterType": "AuthParameter",
1125
+ "parameterName": "Scope",
1126
+ "parameterValue": "scope",
1127
+ "authProvider": None,
1128
+ "certificate": None,
1129
+ "description": None,
1130
+ "externalAuthIdentityProvider": None,
1131
+ "parameterGroup": "DefaultGroup",
1132
+ "sequenceNumber": None,
1133
+ },
1134
+ ],
1135
+ }
1136
+ },
1137
+ status=200,
1138
+ )
1139
+
1140
+ # Mock update external credential
1141
+ responses.add(
1142
+ method="PATCH",
1143
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
1144
+ json={},
1145
+ status=200,
1146
+ )
1147
+
1148
+ task()
1149
+ assert len(responses.calls) == 5 # 2 setup + 3 task calls
1150
+
1151
+ # Verify the PATCH request removed AuthProvider
1152
+ patch_call = responses.calls[4] # Last call is the PATCH
1153
+ updated_params = patch_call.request.body
1154
+ import json
1155
+
1156
+ body = json.loads(updated_params)
1157
+ params = body["Metadata"]["externalCredentialParameters"]
1158
+
1159
+ # Should not contain AuthProvider
1160
+ assert not any(p.get("parameterType") == "AuthProvider" for p in params)
1161
+ # Should contain ExternalAuthIdentityProvider
1162
+ assert any(
1163
+ p.get("parameterType") == "ExternalAuthIdentityProvider" for p in params
1164
+ )
1165
+ # Should still contain AuthParameter
1166
+ assert any(p.get("parameterType") == "AuthParameter" for p in params)
1167
+
1168
+ @responses.activate
1169
+ def test_multiple_auth_providers_removed(self):
1170
+ """Test that multiple conflicting parameters are removed"""
1171
+ # Mock SF API version discovery
1172
+ responses.add(
1173
+ method="GET",
1174
+ url="https://test.salesforce.com/services/data",
1175
+ json=[{"version": CURRENT_SF_API_VERSION}],
1176
+ status=200,
1177
+ )
1178
+
1179
+ # Mock installed packages query
1180
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
1181
+ responses.add(
1182
+ method="GET",
1183
+ url=f"{tooling_url}/query/?q=SELECT+SubscriberPackage.Id%2C+SubscriberPackage.Name%2C+SubscriberPackage.NamespacePrefix%2C+SubscriberPackageVersionId+FROM+InstalledSubscriberPackage",
1184
+ json={"size": 0, "records": []},
1185
+ status=200,
1186
+ )
1187
+
1188
+ task = create_task(
1189
+ UpdateExternalCredential,
1190
+ {
1191
+ "name": "testExtCred",
1192
+ "parameters": [
1193
+ {"external_auth_identity_provider": "MyExternalAuthProvider"}
1194
+ ],
1195
+ },
1196
+ )
1197
+
1198
+ ext_cred_id = "0XE1234567890ABC"
1199
+
1200
+ # Mock query for external credential ID
1201
+ responses.add(
1202
+ method="GET",
1203
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
1204
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
1205
+ status=200,
1206
+ )
1207
+
1208
+ # Mock get external credential object with multiple AuthProvider parameters
1209
+ responses.add(
1210
+ method="GET",
1211
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
1212
+ json={
1213
+ "Metadata": {
1214
+ "externalCredentialParameters": [
1215
+ {
1216
+ "parameterType": "AuthProvider",
1217
+ "parameterName": "AuthProvider",
1218
+ "authProvider": "Provider1",
1219
+ "certificate": None,
1220
+ "description": None,
1221
+ "externalAuthIdentityProvider": None,
1222
+ "parameterGroup": "Group1",
1223
+ "parameterValue": "Provider1",
1224
+ "sequenceNumber": None,
1225
+ },
1226
+ {
1227
+ "parameterType": "AuthProvider",
1228
+ "parameterName": "AuthProvider",
1229
+ "authProvider": "Provider2",
1230
+ "certificate": None,
1231
+ "description": None,
1232
+ "externalAuthIdentityProvider": None,
1233
+ "parameterGroup": "Group2",
1234
+ "parameterValue": "Provider2",
1235
+ "sequenceNumber": None,
1236
+ },
1237
+ ],
1238
+ }
1239
+ },
1240
+ status=200,
1241
+ )
1242
+
1243
+ # Mock update external credential
1244
+ responses.add(
1245
+ method="PATCH",
1246
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
1247
+ json={},
1248
+ status=200,
1249
+ )
1250
+
1251
+ task()
1252
+ assert len(responses.calls) == 5 # 2 setup + 3 task calls
1253
+
1254
+ # Verify all AuthProvider parameters were removed
1255
+ patch_call = responses.calls[4] # Last call is the PATCH
1256
+ updated_params = patch_call.request.body
1257
+ import json
1258
+
1259
+ body = json.loads(updated_params)
1260
+ params = body["Metadata"]["externalCredentialParameters"]
1261
+
1262
+ # Should not contain any AuthProvider
1263
+ auth_providers = [p for p in params if p.get("parameterType") == "AuthProvider"]
1264
+ assert len(auth_providers) == 0
1265
+
1266
+ @responses.activate
1267
+ def test_no_conflict_when_no_existing_parameters(self):
1268
+ """Test that no error occurs when there are no existing conflicting parameters"""
1269
+ # Mock SF API version discovery
1270
+ responses.add(
1271
+ method="GET",
1272
+ url="https://test.salesforce.com/services/data",
1273
+ json=[{"version": CURRENT_SF_API_VERSION}],
1274
+ status=200,
1275
+ )
1276
+
1277
+ # Mock installed packages query
1278
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
1279
+ responses.add(
1280
+ method="GET",
1281
+ url=f"{tooling_url}/query/?q=SELECT+SubscriberPackage.Id%2C+SubscriberPackage.Name%2C+SubscriberPackage.NamespacePrefix%2C+SubscriberPackageVersionId+FROM+InstalledSubscriberPackage",
1282
+ json={"size": 0, "records": []},
1283
+ status=200,
1284
+ )
1285
+
1286
+ task = create_task(
1287
+ UpdateExternalCredential,
1288
+ {
1289
+ "name": "testExtCred",
1290
+ "parameters": [{"auth_provider": "MyAuthProvider"}],
1291
+ },
1292
+ )
1293
+
1294
+ ext_cred_id = "0XE1234567890ABC"
1295
+
1296
+ # Mock query for external credential ID
1297
+ responses.add(
1298
+ method="GET",
1299
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
1300
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
1301
+ status=200,
1302
+ )
1303
+
1304
+ # Mock get external credential object with no parameters
1305
+ responses.add(
1306
+ method="GET",
1307
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
1308
+ json={
1309
+ "Metadata": {
1310
+ "externalCredentialParameters": [],
1311
+ }
1312
+ },
1313
+ status=200,
1314
+ )
1315
+
1316
+ # Mock update external credential
1317
+ responses.add(
1318
+ method="PATCH",
1319
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
1320
+ json={},
1321
+ status=200,
1322
+ )
1323
+
1324
+ task()
1325
+ assert len(responses.calls) == 5 # 2 setup + 3 task calls
1326
+
1327
+ @responses.activate
1328
+ def test_update_existing_auth_provider_removes_external_auth(self):
1329
+ """Test updating existing AuthProvider also removes ExternalAuthIdentityProvider"""
1330
+ # Mock SF API version discovery
1331
+ responses.add(
1332
+ method="GET",
1333
+ url="https://test.salesforce.com/services/data",
1334
+ json=[{"version": CURRENT_SF_API_VERSION}],
1335
+ status=200,
1336
+ )
1337
+
1338
+ # Mock installed packages query
1339
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
1340
+ responses.add(
1341
+ method="GET",
1342
+ url=f"{tooling_url}/query/?q=SELECT+SubscriberPackage.Id%2C+SubscriberPackage.Name%2C+SubscriberPackage.NamespacePrefix%2C+SubscriberPackageVersionId+FROM+InstalledSubscriberPackage",
1343
+ json={"size": 0, "records": []},
1344
+ status=200,
1345
+ )
1346
+
1347
+ task = create_task(
1348
+ UpdateExternalCredential,
1349
+ {
1350
+ "name": "testExtCred",
1351
+ "parameters": [{"auth_provider": "UpdatedAuthProvider"}],
1352
+ },
1353
+ )
1354
+
1355
+ ext_cred_id = "0XE1234567890ABC"
1356
+
1357
+ # Mock query for external credential ID
1358
+ responses.add(
1359
+ method="GET",
1360
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
1361
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
1362
+ status=200,
1363
+ )
1364
+
1365
+ # Mock get external credential object with both AuthProvider and ExternalAuthIdentityProvider
1366
+ responses.add(
1367
+ method="GET",
1368
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
1369
+ json={
1370
+ "Metadata": {
1371
+ "externalCredentialParameters": [
1372
+ {
1373
+ "parameterType": "AuthProvider",
1374
+ "parameterName": "AuthProvider",
1375
+ "authProvider": "OldAuthProvider",
1376
+ "certificate": None,
1377
+ "description": None,
1378
+ "externalAuthIdentityProvider": None,
1379
+ "parameterGroup": "DefaultGroup",
1380
+ "parameterValue": "OldAuthProvider",
1381
+ "sequenceNumber": None,
1382
+ },
1383
+ {
1384
+ "parameterType": "ExternalAuthIdentityProvider",
1385
+ "parameterName": "ExternalAuthIdentityProvider",
1386
+ "externalAuthIdentityProvider": "SomeProvider",
1387
+ "authProvider": None,
1388
+ "certificate": None,
1389
+ "description": None,
1390
+ "parameterGroup": "DefaultGroup",
1391
+ "parameterValue": None,
1392
+ "sequenceNumber": None,
1393
+ },
1394
+ ],
1395
+ }
1396
+ },
1397
+ status=200,
1398
+ )
1399
+
1400
+ # Mock update external credential
1401
+ responses.add(
1402
+ method="PATCH",
1403
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
1404
+ json={},
1405
+ status=200,
1406
+ )
1407
+
1408
+ task()
1409
+ assert len(responses.calls) == 5 # 2 setup + 3 task calls
1410
+
1411
+ # Verify ExternalAuthIdentityProvider was removed
1412
+ patch_call = responses.calls[4] # Last call is the PATCH
1413
+ updated_params = patch_call.request.body
1414
+ import json
1415
+
1416
+ body = json.loads(updated_params)
1417
+ params = body["Metadata"]["externalCredentialParameters"]
1418
+
1419
+ # Should have 2 AuthProviders (old one updated + new one added) and no ExternalAuthIdentityProvider
1420
+ # Actually, since authProvider value is different, it adds a new AuthProvider
1421
+ # So we have old AuthProvider updated to new value, no new one is added
1422
+ auth_providers = [p for p in params if p.get("parameterType") == "AuthProvider"]
1423
+ assert len(auth_providers) >= 1
1424
+ # Should not contain ExternalAuthIdentityProvider
1425
+ assert not any(
1426
+ p.get("parameterType") == "ExternalAuthIdentityProvider" for p in params
1427
+ )