cumulusci-plus 5.0.24__py3-none-any.whl → 5.0.25__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.

Potentially problematic release.


This version of cumulusci-plus might be problematic. Click here for more details.

@@ -0,0 +1,912 @@
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["parameterValue"] == "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 result["parameterValue"] == "EnvAuthProvider"
235
+
236
+ def test_transform_parameter_missing_env(self):
237
+ """Test parameter transformation with missing environment variable"""
238
+ auth_header = HttpHeader(name="Auth", value="NONEXISTENT_ENV_VAR")
239
+ param = TransformExternalCredentialParameter(auth_header=auth_header)
240
+ result = param.get_external_credential_parameter()
241
+ assert result["parameterValue"] is None
242
+
243
+ def test_transform_credential_parameter_from_env(self):
244
+ """Test credential parameter transformation from environment variable"""
245
+ with mock.patch.dict(os.environ, {"CLIENT_SECRET": "secret123"}):
246
+ named_principal = ExternalCredential(
247
+ name="MyPrincipal",
248
+ value="test",
249
+ client_id="client123",
250
+ client_secret="CLIENT_SECRET",
251
+ )
252
+ param = TransformExternalCredentialParameter(
253
+ named_principal=named_principal
254
+ )
255
+ result = param.get_credential_parameter()
256
+ assert result["clientSecret"]["value"] == "secret123"
257
+
258
+
259
+ class TestUpdateExternalCredential:
260
+ """Test UpdateExternalCredential task"""
261
+
262
+ @responses.activate
263
+ def test_update_external_credential_success(self):
264
+ """Test successful update of external credential"""
265
+ auth_header = HttpHeader(name="Authorization", value="Bearer newtoken123")
266
+ task = create_task(
267
+ UpdateExternalCredential,
268
+ {
269
+ "name": "testExtCred",
270
+ "namespace": "",
271
+ "parameters": [{"auth_header": auth_header}],
272
+ },
273
+ )
274
+
275
+ ext_cred_id = "0XE1234567890ABC"
276
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
277
+
278
+ # Mock query for external credential ID
279
+ responses.add(
280
+ method="GET",
281
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
282
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
283
+ status=200,
284
+ )
285
+
286
+ # Mock get external credential object
287
+ responses.add(
288
+ method="GET",
289
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
290
+ json={
291
+ "Metadata": {
292
+ "description": "Old description",
293
+ "externalCredentialParameters": [
294
+ {
295
+ "parameterType": "AuthHeader",
296
+ "parameterValue": "Bearer oldtoken123",
297
+ "description": None,
298
+ "parameterName": None,
299
+ "sequenceNumber": None,
300
+ }
301
+ ],
302
+ }
303
+ },
304
+ status=200,
305
+ )
306
+
307
+ # Mock update external credential
308
+ responses.add(
309
+ method="PATCH",
310
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
311
+ json={},
312
+ status=200,
313
+ )
314
+
315
+ task()
316
+ assert len(responses.calls) == 3
317
+
318
+ @responses.activate
319
+ def test_update_external_credential_with_named_principal(self):
320
+ """Test update with named principal and credential management"""
321
+ named_principal = ExternalCredential(
322
+ name="MyPrincipal",
323
+ value="test-value",
324
+ client_id="client123",
325
+ client_secret="secret456",
326
+ auth_protocol="OAuth2",
327
+ )
328
+ task = create_task(
329
+ UpdateExternalCredential,
330
+ {
331
+ "name": "testExtCred",
332
+ "parameters": [{"named_principal": named_principal}],
333
+ },
334
+ )
335
+
336
+ ext_cred_id = "0XE1234567890ABC"
337
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
338
+ connect_url = (
339
+ f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}"
340
+ )
341
+
342
+ # Mock query for external credential ID
343
+ responses.add(
344
+ method="GET",
345
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
346
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
347
+ status=200,
348
+ )
349
+
350
+ # Mock get external credential object
351
+ responses.add(
352
+ method="GET",
353
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
354
+ json={
355
+ "Metadata": {
356
+ "externalCredentialParameters": [
357
+ {
358
+ "parameterType": "NamedPrincipal",
359
+ "parameterName": "MyPrincipal",
360
+ "parameterValue": None,
361
+ }
362
+ ],
363
+ }
364
+ },
365
+ status=200,
366
+ )
367
+
368
+ # Mock update external credential
369
+ responses.add(
370
+ method="PATCH",
371
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
372
+ json={},
373
+ status=200,
374
+ )
375
+
376
+ # Mock get credential (existing)
377
+ responses.add(
378
+ method="GET",
379
+ url=f"{connect_url}/named-credentials/credential",
380
+ json={
381
+ "principalType": "NamedPrincipal",
382
+ "principalName": "MyPrincipal",
383
+ "externalCredential": "testExtCred",
384
+ "authenticationStatus": "Configured",
385
+ "credentials": {
386
+ "clientId": {
387
+ "value": "client123",
388
+ "encrypted": False,
389
+ },
390
+ },
391
+ },
392
+ status=200,
393
+ )
394
+
395
+ # Mock update credential
396
+ responses.add(
397
+ method="PUT",
398
+ url=f"{connect_url}/named-credentials/credential",
399
+ json={},
400
+ status=200,
401
+ )
402
+
403
+ task()
404
+ assert len(responses.calls) == 5
405
+
406
+ @responses.activate
407
+ def test_update_external_credential_create_new_credential(self):
408
+ """Test creating new credential when it doesn't exist"""
409
+ named_principal = ExternalCredential(
410
+ name="NewPrincipal",
411
+ value="test-value",
412
+ client_id="client123",
413
+ client_secret="secret456",
414
+ )
415
+ task = create_task(
416
+ UpdateExternalCredential,
417
+ {
418
+ "name": "testExtCred",
419
+ "parameters": [{"named_principal": named_principal}],
420
+ },
421
+ )
422
+
423
+ ext_cred_id = "0XE1234567890ABC"
424
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
425
+ connect_url = (
426
+ f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}"
427
+ )
428
+
429
+ # Mock query for external credential ID
430
+ responses.add(
431
+ method="GET",
432
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
433
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
434
+ status=200,
435
+ )
436
+
437
+ # Mock get external credential object
438
+ responses.add(
439
+ method="GET",
440
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
441
+ json={
442
+ "Metadata": {
443
+ "externalCredentialParameters": [],
444
+ }
445
+ },
446
+ status=200,
447
+ )
448
+
449
+ # Mock update external credential
450
+ responses.add(
451
+ method="PATCH",
452
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
453
+ json={},
454
+ status=200,
455
+ )
456
+
457
+ # Mock get credential (not found)
458
+ responses.add(
459
+ method="GET",
460
+ url=f"{connect_url}/named-credentials/credential",
461
+ json={
462
+ "principalType": "NamedPrincipal",
463
+ "principalName": "MyPrincipal",
464
+ "externalCredential": "testExtCred",
465
+ "authenticationStatus": "Configured",
466
+ "credentials": {},
467
+ },
468
+ status=200,
469
+ )
470
+
471
+ # Mock create credential
472
+ responses.add(
473
+ method="POST",
474
+ url=f"{connect_url}/named-credentials/credential",
475
+ json={},
476
+ status=201,
477
+ )
478
+
479
+ task()
480
+ assert len(responses.calls) == 5
481
+
482
+ @responses.activate
483
+ def test_update_external_credential_not_found(self):
484
+ """Test update of non-existent external credential"""
485
+ auth_header = HttpHeader(name="Authorization", value="Bearer token")
486
+ task = create_task(
487
+ UpdateExternalCredential,
488
+ {"name": "nonExistentCred", "parameters": [{"auth_header": auth_header}]},
489
+ )
490
+
491
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
492
+
493
+ # Mock query returning no results
494
+ responses.add(
495
+ method="GET",
496
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27nonExistentCred%27+LIMIT+1",
497
+ json={"size": 0, "records": []},
498
+ status=200,
499
+ )
500
+
501
+ with pytest.raises(
502
+ SalesforceDXException,
503
+ match="External credential 'nonExistentCred' not found",
504
+ ):
505
+ task()
506
+
507
+ @responses.activate
508
+ def test_update_external_credential_with_namespace(self):
509
+ """Test update of external credential with namespace"""
510
+ task = create_task(
511
+ UpdateExternalCredential,
512
+ {
513
+ "name": "testExtCred",
514
+ "namespace": "myns",
515
+ "parameters": [{"auth_provider": "MyAuthProvider"}],
516
+ },
517
+ )
518
+
519
+ ext_cred_id = "0XE1234567890ABC"
520
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
521
+
522
+ # Mock query for external credential ID
523
+ responses.add(
524
+ method="GET",
525
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+AND+NamespacePrefix%3D%27myns%27+LIMIT+1",
526
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
527
+ status=200,
528
+ )
529
+
530
+ # Mock get external credential object
531
+ responses.add(
532
+ method="GET",
533
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
534
+ json={
535
+ "Metadata": {
536
+ "externalCredentialParameters": [
537
+ {
538
+ "parameterType": "AuthProvider",
539
+ "parameterValue": "OldAuthProvider",
540
+ "description": None,
541
+ "parameterName": None,
542
+ "sequenceNumber": None,
543
+ }
544
+ ],
545
+ }
546
+ },
547
+ status=200,
548
+ )
549
+
550
+ # Mock update external credential
551
+ responses.add(
552
+ method="PATCH",
553
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
554
+ json={},
555
+ status=200,
556
+ )
557
+
558
+ task()
559
+ assert len(responses.calls) == 3
560
+
561
+ @responses.activate
562
+ def test_update_external_credential_add_new_parameter(self):
563
+ """Test adding a new parameter to external credential"""
564
+ jwt_claim = ExtParameter(name="sub", value='{"sub":"user123"}')
565
+ task = create_task(
566
+ UpdateExternalCredential,
567
+ {
568
+ "name": "testExtCred",
569
+ "parameters": [{"jwt_body_claim": jwt_claim}],
570
+ },
571
+ )
572
+
573
+ ext_cred_id = "0XE1234567890ABC"
574
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
575
+
576
+ # Mock query for external credential ID
577
+ responses.add(
578
+ method="GET",
579
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
580
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
581
+ status=200,
582
+ )
583
+
584
+ # Mock get external credential object with existing parameter
585
+ responses.add(
586
+ method="GET",
587
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
588
+ json={
589
+ "Metadata": {
590
+ "externalCredentialParameters": [
591
+ {
592
+ "parameterType": "AuthHeader",
593
+ "parameterValue": "Bearer token123",
594
+ "description": None,
595
+ "parameterName": None,
596
+ "sequenceNumber": None,
597
+ }
598
+ ],
599
+ }
600
+ },
601
+ status=200,
602
+ )
603
+
604
+ # Mock update external credential
605
+ responses.add(
606
+ method="PATCH",
607
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
608
+ json={},
609
+ status=200,
610
+ )
611
+
612
+ task()
613
+ assert len(responses.calls) == 3
614
+
615
+ @responses.activate
616
+ def test_update_external_credential_with_transform_parameters(self):
617
+ """Test update with transform parameters from environment variables"""
618
+ with mock.patch.dict(os.environ, {"MY_AUTH_TOKEN": "Bearer envtoken789"}):
619
+ auth_header = HttpHeader(name="Authorization", value="MY_AUTH_TOKEN")
620
+ task = create_task(
621
+ UpdateExternalCredential,
622
+ {
623
+ "name": "testExtCred",
624
+ "transform_parameters": [{"auth_header": auth_header}],
625
+ },
626
+ )
627
+
628
+ ext_cred_id = "0XE1234567890ABC"
629
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
630
+
631
+ # Mock query for external credential ID
632
+ responses.add(
633
+ method="GET",
634
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
635
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
636
+ status=200,
637
+ )
638
+
639
+ # Mock get external credential object
640
+ responses.add(
641
+ method="GET",
642
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
643
+ json={
644
+ "Metadata": {
645
+ "externalCredentialParameters": [
646
+ {
647
+ "parameterType": "AuthHeader",
648
+ "parameterValue": "Bearer oldtoken",
649
+ "description": None,
650
+ "parameterName": None,
651
+ "sequenceNumber": None,
652
+ }
653
+ ],
654
+ }
655
+ },
656
+ status=200,
657
+ )
658
+
659
+ # Mock update external credential
660
+ responses.add(
661
+ method="PATCH",
662
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
663
+ json={},
664
+ status=200,
665
+ )
666
+
667
+ task()
668
+ assert len(responses.calls) == 3
669
+
670
+ @responses.activate
671
+ def test_update_external_credential_retrieve_error(self):
672
+ """Test error handling when retrieving external credential fails"""
673
+ auth_header = HttpHeader(name="Authorization", value="Bearer token")
674
+ task = create_task(
675
+ UpdateExternalCredential,
676
+ {"name": "testExtCred", "parameters": [{"auth_header": auth_header}]},
677
+ )
678
+
679
+ ext_cred_id = "0XE1234567890ABC"
680
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
681
+
682
+ # Mock query for external credential ID
683
+ responses.add(
684
+ method="GET",
685
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
686
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
687
+ status=200,
688
+ )
689
+
690
+ # Mock get external credential object failure
691
+ responses.add(
692
+ method="GET",
693
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
694
+ json={"error": "Not Found"},
695
+ status=404,
696
+ )
697
+
698
+ with pytest.raises(
699
+ SalesforceDXException,
700
+ match="Failed to retrieve external credential object for 'testExtCred'",
701
+ ):
702
+ task()
703
+
704
+ @responses.activate
705
+ def test_update_external_credential_update_error(self):
706
+ """Test error handling when updating external credential fails"""
707
+ auth_header = HttpHeader(name="Authorization", value="Bearer token")
708
+ task = create_task(
709
+ UpdateExternalCredential,
710
+ {"name": "testExtCred", "parameters": [{"auth_header": auth_header}]},
711
+ )
712
+
713
+ ext_cred_id = "0XE1234567890ABC"
714
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
715
+
716
+ # Mock query for external credential ID
717
+ responses.add(
718
+ method="GET",
719
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
720
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
721
+ status=200,
722
+ )
723
+
724
+ # Mock get external credential object
725
+ responses.add(
726
+ method="GET",
727
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
728
+ json={
729
+ "Metadata": {
730
+ "externalCredentialParameters": [
731
+ {
732
+ "parameterType": "AuthHeader",
733
+ "parameterValue": "Bearer oldtoken",
734
+ "description": None,
735
+ "parameterName": None,
736
+ "sequenceNumber": None,
737
+ }
738
+ ],
739
+ }
740
+ },
741
+ status=200,
742
+ )
743
+
744
+ # Mock update external credential failure
745
+ responses.add(
746
+ method="PATCH",
747
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
748
+ json={"error": "Update failed"},
749
+ status=400,
750
+ )
751
+
752
+ with pytest.raises(
753
+ SalesforceDXException,
754
+ match="Failed to update external credential object",
755
+ ):
756
+ task()
757
+
758
+ @responses.activate
759
+ def test_update_external_credential_no_existing_parameters(self):
760
+ """Test update when external credential has no existing parameters"""
761
+ task = create_task(
762
+ UpdateExternalCredential,
763
+ {
764
+ "name": "testExtCred",
765
+ "parameters": [{"auth_provider": "NewAuthProvider"}],
766
+ },
767
+ )
768
+
769
+ ext_cred_id = "0XE1234567890ABC"
770
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
771
+
772
+ # Mock query for external credential ID
773
+ responses.add(
774
+ method="GET",
775
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
776
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
777
+ status=200,
778
+ )
779
+
780
+ # Mock get external credential object with no parameters
781
+ responses.add(
782
+ method="GET",
783
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
784
+ json={"Metadata": {"externalCredentialParameters": []}},
785
+ status=200,
786
+ )
787
+
788
+ # Mock update external credential
789
+ responses.add(
790
+ method="PATCH",
791
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
792
+ json={},
793
+ status=200,
794
+ )
795
+
796
+ task()
797
+ assert len(responses.calls) == 3
798
+
799
+ @responses.activate
800
+ def test_update_external_credential_with_multiple_parameters(self):
801
+ """Test update with multiple parameters"""
802
+ auth_header = HttpHeader(name="Authorization", value="Bearer token123")
803
+ jwt_claim = ExtParameter(name="sub", value='{"sub":"user"}')
804
+ task = create_task(
805
+ UpdateExternalCredential,
806
+ {
807
+ "name": "testExtCred",
808
+ "parameters": [
809
+ {"auth_header": auth_header},
810
+ {"auth_provider": "MyAuthProvider"},
811
+ {"jwt_body_claim": jwt_claim},
812
+ ],
813
+ },
814
+ )
815
+
816
+ ext_cred_id = "0XE1234567890ABC"
817
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
818
+
819
+ # Mock query for external credential ID
820
+ responses.add(
821
+ method="GET",
822
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
823
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
824
+ status=200,
825
+ )
826
+
827
+ # Mock get external credential object
828
+ responses.add(
829
+ method="GET",
830
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
831
+ json={
832
+ "Metadata": {
833
+ "externalCredentialParameters": [
834
+ {
835
+ "parameterType": "AuthHeader",
836
+ "parameterValue": "Bearer oldtoken",
837
+ "description": None,
838
+ "parameterName": None,
839
+ "sequenceNumber": None,
840
+ }
841
+ ],
842
+ }
843
+ },
844
+ status=200,
845
+ )
846
+
847
+ # Mock update external credential
848
+ responses.add(
849
+ method="PATCH",
850
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
851
+ json={},
852
+ status=200,
853
+ )
854
+
855
+ task()
856
+ assert len(responses.calls) == 3
857
+
858
+ @responses.activate
859
+ def test_update_external_credential_with_sequence_number(self):
860
+ """Test update with sequence number"""
861
+ auth_header = HttpHeader(
862
+ name="MyHeader", value="Bearer token123", sequence_number=5
863
+ )
864
+ task = create_task(
865
+ UpdateExternalCredential,
866
+ {
867
+ "name": "testExtCred",
868
+ "parameters": [{"auth_header": auth_header}],
869
+ },
870
+ )
871
+
872
+ ext_cred_id = "0XE1234567890ABC"
873
+ tooling_url = f"https://test.salesforce.com/services/data/v{CURRENT_SF_API_VERSION}/tooling"
874
+
875
+ # Mock query for external credential ID
876
+ responses.add(
877
+ method="GET",
878
+ url=f"{tooling_url}/query/?q=SELECT+Id+FROM+ExternalCredential+WHERE+DeveloperName%3D%27testExtCred%27+LIMIT+1",
879
+ json={"size": 1, "records": [{"Id": ext_cred_id}]},
880
+ status=200,
881
+ )
882
+
883
+ # Mock get external credential object
884
+ responses.add(
885
+ method="GET",
886
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
887
+ json={
888
+ "Metadata": {
889
+ "externalCredentialParameters": [
890
+ {
891
+ "parameterType": "AuthHeader",
892
+ "parameterValue": "Bearer oldtoken",
893
+ "description": None,
894
+ "parameterName": None,
895
+ "sequenceNumber": None,
896
+ }
897
+ ],
898
+ }
899
+ },
900
+ status=200,
901
+ )
902
+
903
+ # Mock update external credential
904
+ responses.add(
905
+ method="PATCH",
906
+ url=f"{tooling_url}/sobjects/ExternalCredential/{ext_cred_id}",
907
+ json={},
908
+ status=200,
909
+ )
910
+
911
+ task()
912
+ assert len(responses.calls) == 3