cumulusci-plus 5.0.35__py3-none-any.whl → 5.0.45__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cumulusci/__about__.py +1 -1
- cumulusci/cli/cci.py +3 -2
- cumulusci/cli/task.py +9 -10
- cumulusci/cli/tests/test_org.py +5 -0
- cumulusci/cli/tests/test_task.py +34 -0
- cumulusci/core/config/__init__.py +1 -0
- cumulusci/core/config/org_config.py +2 -1
- cumulusci/core/config/project_config.py +12 -0
- cumulusci/core/config/scratch_org_config.py +12 -0
- cumulusci/core/config/sfdx_org_config.py +4 -1
- cumulusci/core/config/tests/test_config.py +1 -0
- cumulusci/core/dependencies/base.py +4 -0
- cumulusci/cumulusci.yml +18 -1
- cumulusci/schema/cumulusci.jsonschema.json +5 -0
- cumulusci/tasks/apex/testrunner.py +7 -4
- cumulusci/tasks/bulkdata/tests/test_select_utils.py +20 -0
- cumulusci/tasks/metadata_etl/__init__.py +2 -0
- cumulusci/tasks/metadata_etl/applications.py +256 -0
- cumulusci/tasks/metadata_etl/tests/test_applications.py +710 -0
- cumulusci/tasks/salesforce/insert_record.py +18 -19
- cumulusci/tasks/salesforce/tests/test_enable_prediction.py +4 -2
- cumulusci/tasks/salesforce/tests/test_update_external_auth_identity_provider.py +927 -0
- cumulusci/tasks/salesforce/tests/test_update_external_credential.py +523 -8
- cumulusci/tasks/salesforce/tests/test_update_record.py +512 -0
- cumulusci/tasks/salesforce/update_external_auth_identity_provider.py +551 -0
- cumulusci/tasks/salesforce/update_external_credential.py +89 -4
- cumulusci/tasks/salesforce/update_record.py +217 -0
- cumulusci/tasks/sfdmu/sfdmu.py +14 -1
- cumulusci/tasks/utility/credentialManager.py +58 -12
- cumulusci/tasks/utility/secretsToEnv.py +42 -11
- cumulusci/tasks/utility/tests/test_credentialManager.py +586 -0
- cumulusci/tasks/utility/tests/test_secretsToEnv.py +1240 -62
- cumulusci/utils/yaml/cumulusci_yml.py +1 -0
- {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/METADATA +5 -7
- {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/RECORD +39 -33
- {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/WHEEL +1 -1
- {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/licenses/LICENSE +0 -0
|
@@ -131,6 +131,34 @@ class TestEnvironmentVariableProvider:
|
|
|
131
131
|
result = provider.get_credentials("NONEXISTENT_KEY", {"value": None})
|
|
132
132
|
assert result is None
|
|
133
133
|
|
|
134
|
+
@mock.patch.dict(os.environ, {"MYAPP_API_KEY": "env_api_key_secret"})
|
|
135
|
+
def test_get_credentials_api_key(self):
|
|
136
|
+
"""Test get_credentials retrieves API_KEY secret from environment."""
|
|
137
|
+
provider = EnvironmentVariableProvider(key_prefix="MYAPP_")
|
|
138
|
+
result = provider.get_credentials("API_KEY", {"value": None})
|
|
139
|
+
assert result == "env_api_key_secret"
|
|
140
|
+
|
|
141
|
+
@mock.patch.dict(os.environ, {"TEST_API_KEY": "test_api_value"})
|
|
142
|
+
def test_get_credentials_api_key_with_default(self):
|
|
143
|
+
"""Test get_credentials retrieves API_KEY with default value fallback."""
|
|
144
|
+
provider = EnvironmentVariableProvider(key_prefix="TEST_")
|
|
145
|
+
result = provider.get_credentials("API_KEY", {"value": "default_key"})
|
|
146
|
+
assert result == "test_api_value"
|
|
147
|
+
|
|
148
|
+
@mock.patch.dict(os.environ, {}, clear=True)
|
|
149
|
+
def test_get_credentials_api_key_missing_uses_default(self):
|
|
150
|
+
"""Test get_credentials uses default for missing API_KEY."""
|
|
151
|
+
provider = EnvironmentVariableProvider(key_prefix="MISSING_")
|
|
152
|
+
result = provider.get_credentials("API_KEY", {"value": "fallback_key"})
|
|
153
|
+
assert result == "fallback_key"
|
|
154
|
+
|
|
155
|
+
@mock.patch.dict(os.environ, {}, clear=True)
|
|
156
|
+
def test_get_credentials_api_key_missing_no_default(self):
|
|
157
|
+
"""Test get_credentials returns None for missing API_KEY without default."""
|
|
158
|
+
provider = EnvironmentVariableProvider(key_prefix="MISSING_")
|
|
159
|
+
result = provider.get_credentials("API_KEY", {"value": None})
|
|
160
|
+
assert result is None
|
|
161
|
+
|
|
134
162
|
def test_get_all_credentials_not_supported(self):
|
|
135
163
|
"""Test that get_all_credentials raises NotImplementedError."""
|
|
136
164
|
provider = EnvironmentVariableProvider()
|
|
@@ -367,6 +395,517 @@ class TestAwsSecretsManagerProvider:
|
|
|
367
395
|
|
|
368
396
|
assert result is None
|
|
369
397
|
|
|
398
|
+
def test_get_credentials_with_binary_secret(self):
|
|
399
|
+
"""Test get_credentials with binary secret content."""
|
|
400
|
+
mock_client = mock.Mock()
|
|
401
|
+
mock_session = mock.Mock()
|
|
402
|
+
mock_session.client.return_value = mock_client
|
|
403
|
+
mock_boto3 = mock.Mock()
|
|
404
|
+
mock_boto3.session.Session.return_value = mock_session
|
|
405
|
+
|
|
406
|
+
binary_content = b"binary_secret_content"
|
|
407
|
+
mock_client.get_secret_value.return_value = {"SecretBinary": binary_content}
|
|
408
|
+
|
|
409
|
+
with mock.patch.dict(
|
|
410
|
+
sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
|
|
411
|
+
):
|
|
412
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
413
|
+
|
|
414
|
+
# Mock the create_binary_file method
|
|
415
|
+
with mock.patch.object(
|
|
416
|
+
provider,
|
|
417
|
+
"create_binary_file",
|
|
418
|
+
return_value="/tmp/.cci/my-secret/cert.pem",
|
|
419
|
+
) as mock_create_file:
|
|
420
|
+
result = provider.get_credentials(
|
|
421
|
+
"CERT_FILE", {"secret_name": "my-secret/cert"}
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Verify create_binary_file was called with correct arguments
|
|
425
|
+
mock_create_file.assert_called_once_with(
|
|
426
|
+
"my-secret/cert", binary_content
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Verify the result contains the file path
|
|
430
|
+
assert result == "/tmp/.cci/my-secret/cert.pem"
|
|
431
|
+
|
|
432
|
+
def test_get_credentials_with_binary_secret_wildcard_key(self):
|
|
433
|
+
"""Test get_all_credentials with binary secret when key is '*' uses basename."""
|
|
434
|
+
mock_client = mock.Mock()
|
|
435
|
+
mock_session = mock.Mock()
|
|
436
|
+
mock_session.client.return_value = mock_client
|
|
437
|
+
mock_boto3 = mock.Mock()
|
|
438
|
+
mock_boto3.session.Session.return_value = mock_session
|
|
439
|
+
|
|
440
|
+
binary_content = b"binary_secret_content"
|
|
441
|
+
mock_client.get_secret_value.return_value = {"SecretBinary": binary_content}
|
|
442
|
+
|
|
443
|
+
with mock.patch.dict(
|
|
444
|
+
sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
|
|
445
|
+
):
|
|
446
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
447
|
+
|
|
448
|
+
with mock.patch.object(
|
|
449
|
+
provider,
|
|
450
|
+
"create_binary_file",
|
|
451
|
+
return_value="/absolute/path/to/.cci/my-secret/file.key",
|
|
452
|
+
) as mock_create_file, mock.patch(
|
|
453
|
+
"os.path.basename", return_value="file.key"
|
|
454
|
+
):
|
|
455
|
+
# Use wildcard key with get_all_credentials
|
|
456
|
+
result = provider.get_all_credentials(
|
|
457
|
+
"*", {"secret_name": "my-secret/file"}
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
mock_create_file.assert_called_once_with(
|
|
461
|
+
"my-secret/file", binary_content
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# When key is "*", should use basename of file path as the key in dict
|
|
465
|
+
assert isinstance(result, dict)
|
|
466
|
+
assert "file.key" in result
|
|
467
|
+
assert result["file.key"] == "/absolute/path/to/.cci/my-secret/file.key"
|
|
468
|
+
|
|
469
|
+
def test_get_all_credentials_with_binary_secret(self):
|
|
470
|
+
"""Test get_all_credentials returns dict with file path for binary secrets."""
|
|
471
|
+
mock_client = mock.Mock()
|
|
472
|
+
mock_session = mock.Mock()
|
|
473
|
+
mock_session.client.return_value = mock_client
|
|
474
|
+
mock_boto3 = mock.Mock()
|
|
475
|
+
mock_boto3.session.Session.return_value = mock_session
|
|
476
|
+
|
|
477
|
+
binary_content = b"certificate_data"
|
|
478
|
+
mock_client.get_secret_value.return_value = {"SecretBinary": binary_content}
|
|
479
|
+
|
|
480
|
+
with mock.patch.dict(
|
|
481
|
+
sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
|
|
482
|
+
):
|
|
483
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
484
|
+
|
|
485
|
+
with mock.patch.object(
|
|
486
|
+
provider,
|
|
487
|
+
"create_binary_file",
|
|
488
|
+
return_value="/path/to/.cci/certs/ca.crt",
|
|
489
|
+
):
|
|
490
|
+
result = provider.get_all_credentials(
|
|
491
|
+
"SSL_CERT", {"secret_name": "certs/ca"}
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Should return a dict with key mapped to file path
|
|
495
|
+
assert isinstance(result, dict)
|
|
496
|
+
assert "SSL_CERT" in result
|
|
497
|
+
assert result["SSL_CERT"] == "/path/to/.cci/certs/ca.crt"
|
|
498
|
+
|
|
499
|
+
def test_get_credentials_raises_error_for_invalid_secret_response(self):
|
|
500
|
+
"""Test get_credentials raises ValueError when response has neither SecretString nor SecretBinary."""
|
|
501
|
+
# Create a dummy ClientError class for exception handling
|
|
502
|
+
class ClientError(Exception):
|
|
503
|
+
pass
|
|
504
|
+
|
|
505
|
+
mock_client = mock.Mock()
|
|
506
|
+
mock_session = mock.Mock()
|
|
507
|
+
mock_session.client.return_value = mock_client
|
|
508
|
+
mock_boto3 = mock.Mock()
|
|
509
|
+
mock_boto3.session.Session.return_value = mock_session
|
|
510
|
+
|
|
511
|
+
# Return a response with neither SecretString nor SecretBinary
|
|
512
|
+
mock_client.get_secret_value.return_value = {}
|
|
513
|
+
|
|
514
|
+
# Create a proper mock for botocore.exceptions
|
|
515
|
+
mock_botocore_exceptions = type(sys)("botocore.exceptions")
|
|
516
|
+
mock_botocore_exceptions.ClientError = ClientError
|
|
517
|
+
|
|
518
|
+
with mock.patch.dict(
|
|
519
|
+
sys.modules,
|
|
520
|
+
{"boto3": mock_boto3, "botocore.exceptions": mock_botocore_exceptions},
|
|
521
|
+
):
|
|
522
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
523
|
+
|
|
524
|
+
with pytest.raises(ValueError) as exc_info:
|
|
525
|
+
provider.get_credentials("API_KEY", {"secret_name": "invalid-secret"})
|
|
526
|
+
|
|
527
|
+
assert "is not a valid secret" in str(exc_info.value)
|
|
528
|
+
assert "invalid-secret" in str(exc_info.value)
|
|
529
|
+
|
|
530
|
+
def test_create_binary_file_success(self):
|
|
531
|
+
"""Test create_binary_file creates file successfully."""
|
|
532
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
533
|
+
|
|
534
|
+
binary_content = b"test_binary_content"
|
|
535
|
+
secret_name = "test-secret/file"
|
|
536
|
+
expected_path = "/abs/path/.cci/test-secret/file"
|
|
537
|
+
|
|
538
|
+
with mock.patch("os.makedirs") as mock_makedirs, mock.patch(
|
|
539
|
+
"builtins.open", mock.mock_open()
|
|
540
|
+
) as mock_file, mock.patch(
|
|
541
|
+
"os.path.abspath", return_value=expected_path
|
|
542
|
+
), mock.patch(
|
|
543
|
+
"os.path.dirname", return_value="/abs/path/.cci/test-secret"
|
|
544
|
+
), mock.patch(
|
|
545
|
+
"os.path.join", return_value=".cci/test-secret/file"
|
|
546
|
+
):
|
|
547
|
+
|
|
548
|
+
result = provider.create_binary_file(secret_name, binary_content)
|
|
549
|
+
|
|
550
|
+
# Verify directory creation with exist_ok=True
|
|
551
|
+
mock_makedirs.assert_called_once_with(
|
|
552
|
+
"/abs/path/.cci/test-secret", exist_ok=True
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Verify file was opened in binary write mode
|
|
556
|
+
mock_file.assert_called_once_with(expected_path, "wb")
|
|
557
|
+
|
|
558
|
+
# Verify content was written
|
|
559
|
+
mock_file().write.assert_called_once_with(binary_content)
|
|
560
|
+
|
|
561
|
+
# Verify return value is absolute path
|
|
562
|
+
assert result == expected_path
|
|
563
|
+
|
|
564
|
+
@mock.patch("os.name", "posix")
|
|
565
|
+
def test_create_binary_file_linux_path_separator(self):
|
|
566
|
+
"""Test create_binary_file uses Linux path separator."""
|
|
567
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
568
|
+
|
|
569
|
+
binary_content = b"test_content"
|
|
570
|
+
secret_name = "secrets/database/cert"
|
|
571
|
+
expected_path = "/home/user/project/.cci/secrets/database/cert"
|
|
572
|
+
|
|
573
|
+
with mock.patch("os.makedirs"), mock.patch(
|
|
574
|
+
"builtins.open", mock.mock_open()
|
|
575
|
+
), mock.patch("os.path.abspath", return_value=expected_path), mock.patch(
|
|
576
|
+
"os.path.dirname", return_value="/home/user/project/.cci/secrets/database"
|
|
577
|
+
), mock.patch(
|
|
578
|
+
"os.path.join", return_value=".cci/secrets/database/cert"
|
|
579
|
+
):
|
|
580
|
+
|
|
581
|
+
result = provider.create_binary_file(secret_name, binary_content)
|
|
582
|
+
|
|
583
|
+
# On Linux, path should use forward slashes
|
|
584
|
+
assert "/" in result
|
|
585
|
+
assert "\\" not in result
|
|
586
|
+
assert result == expected_path
|
|
587
|
+
|
|
588
|
+
@mock.patch("os.name", "nt")
|
|
589
|
+
def test_create_binary_file_windows_path_separator(self):
|
|
590
|
+
"""Test create_binary_file uses Windows path separator."""
|
|
591
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
592
|
+
|
|
593
|
+
binary_content = b"test_content"
|
|
594
|
+
secret_name = "secrets/database/cert"
|
|
595
|
+
expected_path = "C:\\Users\\user\\project\\.cci\\secrets\\database\\cert"
|
|
596
|
+
|
|
597
|
+
with mock.patch("os.makedirs"), mock.patch(
|
|
598
|
+
"builtins.open", mock.mock_open()
|
|
599
|
+
), mock.patch("os.path.abspath", return_value=expected_path), mock.patch(
|
|
600
|
+
"os.path.dirname",
|
|
601
|
+
return_value="C:\\Users\\user\\project\\.cci\\secrets\\database",
|
|
602
|
+
), mock.patch(
|
|
603
|
+
"os.path.join", return_value=".cci\\secrets\\database\\cert"
|
|
604
|
+
):
|
|
605
|
+
|
|
606
|
+
result = provider.create_binary_file(secret_name, binary_content)
|
|
607
|
+
|
|
608
|
+
# On Windows, path should use backslashes
|
|
609
|
+
assert "\\" in result
|
|
610
|
+
assert result == expected_path
|
|
611
|
+
|
|
612
|
+
def test_create_binary_file_creates_nested_directories(self):
|
|
613
|
+
"""Test create_binary_file creates nested directories if they don't exist."""
|
|
614
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
615
|
+
|
|
616
|
+
binary_content = b"test_content"
|
|
617
|
+
secret_name = "deep/nested/path/secret"
|
|
618
|
+
|
|
619
|
+
with mock.patch("os.makedirs") as mock_makedirs, mock.patch(
|
|
620
|
+
"builtins.open", mock.mock_open()
|
|
621
|
+
), mock.patch(
|
|
622
|
+
"os.path.abspath", return_value="/path/.cci/deep/nested/path/secret"
|
|
623
|
+
), mock.patch(
|
|
624
|
+
"os.path.dirname", return_value="/path/.cci/deep/nested/path"
|
|
625
|
+
):
|
|
626
|
+
|
|
627
|
+
provider.create_binary_file(secret_name, binary_content)
|
|
628
|
+
|
|
629
|
+
# Verify makedirs was called with exist_ok=True
|
|
630
|
+
mock_makedirs.assert_called_once_with(
|
|
631
|
+
"/path/.cci/deep/nested/path", exist_ok=True
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
def test_create_binary_file_handles_write_error(self):
|
|
635
|
+
"""Test create_binary_file raises RuntimeError when file write fails."""
|
|
636
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
637
|
+
|
|
638
|
+
binary_content = b"test_content"
|
|
639
|
+
secret_name = "test-secret/file"
|
|
640
|
+
expected_path = "/abs/path/.cci/test-secret/file"
|
|
641
|
+
|
|
642
|
+
with mock.patch("os.makedirs"), mock.patch(
|
|
643
|
+
"builtins.open", side_effect=IOError("Permission denied")
|
|
644
|
+
), mock.patch("os.path.abspath", return_value=expected_path), mock.patch(
|
|
645
|
+
"os.path.dirname", return_value="/abs/path/.cci/test-secret"
|
|
646
|
+
), mock.patch(
|
|
647
|
+
"os.path.join", return_value=".cci/test-secret/file"
|
|
648
|
+
):
|
|
649
|
+
|
|
650
|
+
with pytest.raises(RuntimeError) as exc_info:
|
|
651
|
+
provider.create_binary_file(secret_name, binary_content)
|
|
652
|
+
|
|
653
|
+
assert "Failed to create binary file" in str(exc_info.value)
|
|
654
|
+
assert expected_path in str(exc_info.value)
|
|
655
|
+
|
|
656
|
+
def test_create_binary_file_handles_directory_creation_error(self):
|
|
657
|
+
"""Test create_binary_file raises RuntimeError when directory creation fails."""
|
|
658
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
659
|
+
|
|
660
|
+
binary_content = b"test_content"
|
|
661
|
+
secret_name = "test-secret/file"
|
|
662
|
+
|
|
663
|
+
with mock.patch(
|
|
664
|
+
"os.makedirs", side_effect=OSError("No space left on device")
|
|
665
|
+
), mock.patch(
|
|
666
|
+
"os.path.abspath", return_value="/abs/path/.cci/test-secret/file"
|
|
667
|
+
):
|
|
668
|
+
|
|
669
|
+
with pytest.raises(RuntimeError) as exc_info:
|
|
670
|
+
provider.create_binary_file(secret_name, binary_content)
|
|
671
|
+
|
|
672
|
+
assert "Failed to create binary file" in str(exc_info.value)
|
|
673
|
+
|
|
674
|
+
def test_binary_secret_caching(self):
|
|
675
|
+
"""Test that binary secrets are cached properly."""
|
|
676
|
+
mock_client = mock.Mock()
|
|
677
|
+
mock_session = mock.Mock()
|
|
678
|
+
mock_session.client.return_value = mock_client
|
|
679
|
+
mock_boto3 = mock.Mock()
|
|
680
|
+
mock_boto3.session.Session.return_value = mock_session
|
|
681
|
+
|
|
682
|
+
binary_content = b"cached_binary_content"
|
|
683
|
+
mock_client.get_secret_value.return_value = {"SecretBinary": binary_content}
|
|
684
|
+
|
|
685
|
+
with mock.patch.dict(
|
|
686
|
+
sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
|
|
687
|
+
):
|
|
688
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
689
|
+
|
|
690
|
+
with mock.patch.object(
|
|
691
|
+
provider, "create_binary_file", return_value="/path/to/.cci/my-cert.pem"
|
|
692
|
+
) as mock_create_file:
|
|
693
|
+
# First call - should hit AWS and create file
|
|
694
|
+
result1 = provider.get_credentials(
|
|
695
|
+
"CERT", {"secret_name": "my-app/cert"}
|
|
696
|
+
)
|
|
697
|
+
assert result1 == "/path/to/.cci/my-cert.pem"
|
|
698
|
+
|
|
699
|
+
# Second call - should use cache
|
|
700
|
+
result2 = provider.get_credentials(
|
|
701
|
+
"CERT", {"secret_name": "my-app/cert"}
|
|
702
|
+
)
|
|
703
|
+
assert result2 == "/path/to/.cci/my-cert.pem"
|
|
704
|
+
|
|
705
|
+
# Should only call AWS and create file once
|
|
706
|
+
assert mock_client.get_secret_value.call_count == 1
|
|
707
|
+
assert mock_create_file.call_count == 1
|
|
708
|
+
|
|
709
|
+
def test_get_all_credentials_with_binary_secret_empty_key(self):
|
|
710
|
+
"""Test get_all_credentials with binary secret when key is empty string uses basename."""
|
|
711
|
+
mock_client = mock.Mock()
|
|
712
|
+
mock_session = mock.Mock()
|
|
713
|
+
mock_session.client.return_value = mock_client
|
|
714
|
+
mock_boto3 = mock.Mock()
|
|
715
|
+
mock_boto3.session.Session.return_value = mock_session
|
|
716
|
+
|
|
717
|
+
binary_content = b"binary_secret_content"
|
|
718
|
+
mock_client.get_secret_value.return_value = {"SecretBinary": binary_content}
|
|
719
|
+
|
|
720
|
+
with mock.patch.dict(
|
|
721
|
+
sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
|
|
722
|
+
):
|
|
723
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
724
|
+
|
|
725
|
+
with mock.patch.object(
|
|
726
|
+
provider,
|
|
727
|
+
"create_binary_file",
|
|
728
|
+
return_value="/path/to/.cci/my-secret/cert.pem",
|
|
729
|
+
) as mock_create_file, mock.patch(
|
|
730
|
+
"os.path.basename", return_value="cert.pem"
|
|
731
|
+
):
|
|
732
|
+
# Use empty string as key with get_all_credentials
|
|
733
|
+
result = provider.get_all_credentials(
|
|
734
|
+
"", {"secret_name": "my-secret/cert"}
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
mock_create_file.assert_called_once_with(
|
|
738
|
+
"my-secret/cert", binary_content
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
# When key is empty, should use basename of file path as dict key
|
|
742
|
+
assert isinstance(result, dict)
|
|
743
|
+
assert "cert.pem" in result
|
|
744
|
+
assert result["cert.pem"] == "/path/to/.cci/my-secret/cert.pem"
|
|
745
|
+
|
|
746
|
+
def test_get_credentials_with_binary_secret_normal_key(self):
|
|
747
|
+
"""Test get_credentials with binary secret when key is provided uses the key."""
|
|
748
|
+
mock_client = mock.Mock()
|
|
749
|
+
mock_session = mock.Mock()
|
|
750
|
+
mock_session.client.return_value = mock_client
|
|
751
|
+
mock_boto3 = mock.Mock()
|
|
752
|
+
mock_boto3.session.Session.return_value = mock_session
|
|
753
|
+
|
|
754
|
+
binary_content = b"binary_secret_content"
|
|
755
|
+
mock_client.get_secret_value.return_value = {"SecretBinary": binary_content}
|
|
756
|
+
|
|
757
|
+
with mock.patch.dict(
|
|
758
|
+
sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
|
|
759
|
+
):
|
|
760
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
761
|
+
|
|
762
|
+
with mock.patch.object(
|
|
763
|
+
provider,
|
|
764
|
+
"create_binary_file",
|
|
765
|
+
return_value="/path/to/.cci/my-secret/cert.pem",
|
|
766
|
+
) as mock_create_file:
|
|
767
|
+
# Use normal key
|
|
768
|
+
result = provider.get_credentials(
|
|
769
|
+
"MY_CERT", {"secret_name": "my-secret/cert"}
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
mock_create_file.assert_called_once_with(
|
|
773
|
+
"my-secret/cert", binary_content
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
# When key is provided and not "*", should use the key directly
|
|
777
|
+
assert result == "/path/to/.cci/my-secret/cert.pem"
|
|
778
|
+
|
|
779
|
+
@mock.patch("os.name", "posix")
|
|
780
|
+
def test_get_credentials_with_binary_secret_linux(self):
|
|
781
|
+
"""Test get_credentials with binary secret on Linux system."""
|
|
782
|
+
mock_client = mock.Mock()
|
|
783
|
+
mock_session = mock.Mock()
|
|
784
|
+
mock_session.client.return_value = mock_client
|
|
785
|
+
mock_boto3 = mock.Mock()
|
|
786
|
+
mock_boto3.session.Session.return_value = mock_session
|
|
787
|
+
|
|
788
|
+
binary_content = b"linux_binary_content"
|
|
789
|
+
mock_client.get_secret_value.return_value = {"SecretBinary": binary_content}
|
|
790
|
+
|
|
791
|
+
with mock.patch.dict(
|
|
792
|
+
sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
|
|
793
|
+
):
|
|
794
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
795
|
+
|
|
796
|
+
linux_path = "/home/user/project/.cci/secrets/database/cert.pem"
|
|
797
|
+
with mock.patch.object(
|
|
798
|
+
provider, "create_binary_file", return_value=linux_path
|
|
799
|
+
) as mock_create_file:
|
|
800
|
+
result = provider.get_credentials(
|
|
801
|
+
"DB_CERT", {"secret_name": "secrets/database/cert"}
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
mock_create_file.assert_called_once_with(
|
|
805
|
+
"secrets/database/cert", binary_content
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
# Verify Linux path format
|
|
809
|
+
assert "/" in result
|
|
810
|
+
assert "\\" not in result
|
|
811
|
+
assert result == linux_path
|
|
812
|
+
|
|
813
|
+
@mock.patch("os.name", "nt")
|
|
814
|
+
def test_get_credentials_with_binary_secret_windows(self):
|
|
815
|
+
"""Test get_credentials with binary secret on Windows system."""
|
|
816
|
+
mock_client = mock.Mock()
|
|
817
|
+
mock_session = mock.Mock()
|
|
818
|
+
mock_session.client.return_value = mock_client
|
|
819
|
+
mock_boto3 = mock.Mock()
|
|
820
|
+
mock_boto3.session.Session.return_value = mock_session
|
|
821
|
+
|
|
822
|
+
binary_content = b"windows_binary_content"
|
|
823
|
+
mock_client.get_secret_value.return_value = {"SecretBinary": binary_content}
|
|
824
|
+
|
|
825
|
+
with mock.patch.dict(
|
|
826
|
+
sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
|
|
827
|
+
):
|
|
828
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
829
|
+
|
|
830
|
+
windows_path = "C:\\Users\\user\\project\\.cci\\secrets\\database\\cert.pem"
|
|
831
|
+
with mock.patch.object(
|
|
832
|
+
provider, "create_binary_file", return_value=windows_path
|
|
833
|
+
) as mock_create_file:
|
|
834
|
+
result = provider.get_credentials(
|
|
835
|
+
"DB_CERT", {"secret_name": "secrets/database/cert"}
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
mock_create_file.assert_called_once_with(
|
|
839
|
+
"secrets/database/cert", binary_content
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
# Verify Windows path format
|
|
843
|
+
assert "\\" in result
|
|
844
|
+
assert result == windows_path
|
|
845
|
+
|
|
846
|
+
@mock.patch("os.name", "posix")
|
|
847
|
+
def test_get_all_credentials_with_binary_secret_linux(self):
|
|
848
|
+
"""Test get_all_credentials with binary secret on Linux system."""
|
|
849
|
+
mock_client = mock.Mock()
|
|
850
|
+
mock_session = mock.Mock()
|
|
851
|
+
mock_session.client.return_value = mock_client
|
|
852
|
+
mock_boto3 = mock.Mock()
|
|
853
|
+
mock_boto3.session.Session.return_value = mock_session
|
|
854
|
+
|
|
855
|
+
binary_content = b"linux_certificate_data"
|
|
856
|
+
mock_client.get_secret_value.return_value = {"SecretBinary": binary_content}
|
|
857
|
+
|
|
858
|
+
with mock.patch.dict(
|
|
859
|
+
sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
|
|
860
|
+
):
|
|
861
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
862
|
+
|
|
863
|
+
linux_path = "/home/user/project/.cci/certs/ca.crt"
|
|
864
|
+
with mock.patch.object(
|
|
865
|
+
provider, "create_binary_file", return_value=linux_path
|
|
866
|
+
):
|
|
867
|
+
result = provider.get_all_credentials(
|
|
868
|
+
"SSL_CERT", {"secret_name": "certs/ca"}
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
# Should return a dict with key mapped to file path
|
|
872
|
+
assert isinstance(result, dict)
|
|
873
|
+
assert "SSL_CERT" in result
|
|
874
|
+
assert "/" in result["SSL_CERT"]
|
|
875
|
+
assert "\\" not in result["SSL_CERT"]
|
|
876
|
+
assert result["SSL_CERT"] == linux_path
|
|
877
|
+
|
|
878
|
+
@mock.patch("os.name", "nt")
|
|
879
|
+
def test_get_all_credentials_with_binary_secret_windows(self):
|
|
880
|
+
"""Test get_all_credentials with binary secret on Windows system."""
|
|
881
|
+
mock_client = mock.Mock()
|
|
882
|
+
mock_session = mock.Mock()
|
|
883
|
+
mock_session.client.return_value = mock_client
|
|
884
|
+
mock_boto3 = mock.Mock()
|
|
885
|
+
mock_boto3.session.Session.return_value = mock_session
|
|
886
|
+
|
|
887
|
+
binary_content = b"windows_certificate_data"
|
|
888
|
+
mock_client.get_secret_value.return_value = {"SecretBinary": binary_content}
|
|
889
|
+
|
|
890
|
+
with mock.patch.dict(
|
|
891
|
+
sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
|
|
892
|
+
):
|
|
893
|
+
provider = AwsSecretsManagerProvider(aws_region="us-east-1")
|
|
894
|
+
|
|
895
|
+
windows_path = "C:\\Users\\user\\project\\.cci\\certs\\ca.crt"
|
|
896
|
+
with mock.patch.object(
|
|
897
|
+
provider, "create_binary_file", return_value=windows_path
|
|
898
|
+
):
|
|
899
|
+
result = provider.get_all_credentials(
|
|
900
|
+
"SSL_CERT", {"secret_name": "certs/ca"}
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
# Should return a dict with key mapped to file path
|
|
904
|
+
assert isinstance(result, dict)
|
|
905
|
+
assert "SSL_CERT" in result
|
|
906
|
+
assert "\\" in result["SSL_CERT"]
|
|
907
|
+
assert result["SSL_CERT"] == windows_path
|
|
908
|
+
|
|
370
909
|
|
|
371
910
|
class TestAzureVariableGroupProvider:
|
|
372
911
|
"""Test cases for AzureVariableGroupProvider."""
|
|
@@ -410,6 +949,27 @@ class TestAzureVariableGroupProvider:
|
|
|
410
949
|
result = provider.get_credentials("my.var.name", {})
|
|
411
950
|
assert result == "value123"
|
|
412
951
|
|
|
952
|
+
@mock.patch.dict(os.environ, {"MYAPP_API_KEY": "azure_api_key_secret"})
|
|
953
|
+
def test_get_credentials_api_key(self):
|
|
954
|
+
"""Test get_credentials retrieves API_KEY secret from Azure variables."""
|
|
955
|
+
provider = AzureVariableGroupProvider(key_prefix="MYAPP_")
|
|
956
|
+
result = provider.get_credentials("API_KEY", {"value": None})
|
|
957
|
+
assert result == "azure_api_key_secret"
|
|
958
|
+
|
|
959
|
+
@mock.patch.dict(os.environ, {"TEST_API_KEY": "test_api_value"})
|
|
960
|
+
def test_get_credentials_api_key_with_default(self):
|
|
961
|
+
"""Test get_credentials retrieves API_KEY with default value fallback."""
|
|
962
|
+
provider = AzureVariableGroupProvider(key_prefix="TEST_")
|
|
963
|
+
result = provider.get_credentials("API_KEY", {"value": "default_key"})
|
|
964
|
+
assert result == "test_api_value"
|
|
965
|
+
|
|
966
|
+
@mock.patch.dict(os.environ, {}, clear=True)
|
|
967
|
+
def test_get_credentials_api_key_missing_no_default(self):
|
|
968
|
+
"""Test get_credentials returns None for missing API_KEY without default."""
|
|
969
|
+
provider = AzureVariableGroupProvider(key_prefix="MISSING_")
|
|
970
|
+
result = provider.get_credentials("API_KEY", {"value": None})
|
|
971
|
+
assert result is None
|
|
972
|
+
|
|
413
973
|
def test_get_all_credentials_not_supported(self):
|
|
414
974
|
"""Test that get_all_credentials raises NotImplementedError."""
|
|
415
975
|
provider = AzureVariableGroupProvider()
|
|
@@ -562,3 +1122,29 @@ class TestProviderIntegration:
|
|
|
562
1122
|
provider = CredentialManager.get_provider(key_prefix="MYAPP_")
|
|
563
1123
|
credentials = provider.get_credentials("API_TOKEN", {})
|
|
564
1124
|
assert credentials == "ado_token_xyz"
|
|
1125
|
+
|
|
1126
|
+
@mock.patch.dict(
|
|
1127
|
+
os.environ,
|
|
1128
|
+
{
|
|
1129
|
+
"CUMULUSCI_SECRETS_TYPE": "ado_variables",
|
|
1130
|
+
"MYAPP_API_KEY": "ado_api_key_123",
|
|
1131
|
+
},
|
|
1132
|
+
)
|
|
1133
|
+
def test_full_workflow_ado_provider_api_key(self):
|
|
1134
|
+
"""Test complete workflow using Azure DevOps variables provider with API_KEY."""
|
|
1135
|
+
provider = CredentialManager.get_provider(key_prefix="MYAPP_")
|
|
1136
|
+
credentials = provider.get_credentials("API_KEY", {"value": None})
|
|
1137
|
+
assert credentials == "ado_api_key_123"
|
|
1138
|
+
|
|
1139
|
+
@mock.patch.dict(
|
|
1140
|
+
os.environ,
|
|
1141
|
+
{
|
|
1142
|
+
"CUMULUSCI_SECRETS_TYPE": "environment",
|
|
1143
|
+
"MYAPP_API_KEY": "env_api_key_456",
|
|
1144
|
+
},
|
|
1145
|
+
)
|
|
1146
|
+
def test_full_workflow_environment_provider_api_key(self):
|
|
1147
|
+
"""Test complete workflow using environment provider with API_KEY."""
|
|
1148
|
+
provider = CredentialManager.get_provider(key_prefix="MYAPP_")
|
|
1149
|
+
credentials = provider.get_credentials("API_KEY", {"value": "default_key"})
|
|
1150
|
+
assert credentials == "env_api_key_456"
|