cumulusci-plus 5.0.35__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 (37) hide show
  1. cumulusci/__about__.py +1 -1
  2. cumulusci/cli/task.py +9 -10
  3. cumulusci/cli/tests/test_org.py +5 -0
  4. cumulusci/cli/tests/test_task.py +34 -0
  5. cumulusci/core/config/__init__.py +1 -0
  6. cumulusci/core/config/org_config.py +2 -1
  7. cumulusci/core/config/project_config.py +12 -0
  8. cumulusci/core/config/scratch_org_config.py +12 -0
  9. cumulusci/core/config/tests/test_config.py +1 -0
  10. cumulusci/core/dependencies/base.py +4 -0
  11. cumulusci/cumulusci.yml +18 -1
  12. cumulusci/schema/cumulusci.jsonschema.json +5 -0
  13. cumulusci/tasks/apex/testrunner.py +7 -4
  14. cumulusci/tasks/bulkdata/tests/test_select_utils.py +20 -0
  15. cumulusci/tasks/metadata_etl/__init__.py +2 -0
  16. cumulusci/tasks/metadata_etl/applications.py +256 -0
  17. cumulusci/tasks/metadata_etl/tests/test_applications.py +710 -0
  18. cumulusci/tasks/salesforce/insert_record.py +18 -19
  19. cumulusci/tasks/salesforce/tests/test_enable_prediction.py +4 -2
  20. cumulusci/tasks/salesforce/tests/test_update_external_auth_identity_provider.py +927 -0
  21. cumulusci/tasks/salesforce/tests/test_update_external_credential.py +523 -8
  22. cumulusci/tasks/salesforce/tests/test_update_record.py +512 -0
  23. cumulusci/tasks/salesforce/update_external_auth_identity_provider.py +551 -0
  24. cumulusci/tasks/salesforce/update_external_credential.py +89 -4
  25. cumulusci/tasks/salesforce/update_record.py +217 -0
  26. cumulusci/tasks/sfdmu/sfdmu.py +14 -1
  27. cumulusci/tasks/utility/credentialManager.py +58 -12
  28. cumulusci/tasks/utility/secretsToEnv.py +2 -2
  29. cumulusci/tasks/utility/tests/test_credentialManager.py +586 -0
  30. cumulusci/tasks/utility/tests/test_secretsToEnv.py +42 -15
  31. cumulusci/utils/yaml/cumulusci_yml.py +1 -0
  32. {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.43.dist-info}/METADATA +6 -7
  33. {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.43.dist-info}/RECORD +37 -31
  34. {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.43.dist-info}/WHEEL +1 -1
  35. {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.43.dist-info}/entry_points.txt +0 -0
  36. {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/AUTHORS.rst +0 -0
  37. {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.43.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"