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

Files changed (39) hide show
  1. cumulusci/__about__.py +1 -1
  2. cumulusci/cli/tests/test_error.py +3 -1
  3. cumulusci/core/flowrunner.py +2 -0
  4. cumulusci/core/github.py +1 -1
  5. cumulusci/core/sfdx.py +3 -1
  6. cumulusci/core/tests/test_flowrunner.py +100 -0
  7. cumulusci/cumulusci.yml +8 -0
  8. cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
  9. cumulusci/salesforce_api/rest_deploy.py +1 -1
  10. cumulusci/tasks/apex/anon.py +1 -1
  11. cumulusci/tasks/apex/testrunner.py +6 -1
  12. cumulusci/tasks/bulkdata/extract.py +0 -1
  13. cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
  14. cumulusci/tasks/bulkdata/tests/test_select_utils.py +6 -0
  15. cumulusci/tasks/metadata_etl/base.py +7 -3
  16. cumulusci/tasks/push/README.md +15 -17
  17. cumulusci/tasks/release_notes/README.md +13 -13
  18. cumulusci/tasks/robotframework/tests/test_robotframework.py +1 -1
  19. cumulusci/tasks/salesforce/Deploy.py +5 -1
  20. cumulusci/tasks/salesforce/composite.py +1 -1
  21. cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
  22. cumulusci/tasks/salesforce/enable_prediction.py +5 -1
  23. cumulusci/tasks/salesforce/sourcetracking.py +1 -1
  24. cumulusci/tasks/salesforce/update_profile.py +17 -13
  25. cumulusci/tasks/salesforce/users/permsets.py +16 -9
  26. cumulusci/tasks/utility/credentialManager.py +256 -0
  27. cumulusci/tasks/utility/directoryRecreator.py +30 -0
  28. cumulusci/tasks/utility/secretsToEnv.py +132 -0
  29. cumulusci/tasks/utility/tests/test_credentialManager.py +564 -0
  30. cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
  31. cumulusci/tasks/utility/tests/test_secretsToEnv.py +1091 -0
  32. cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
  33. cumulusci/utils/yaml/tests/test_model_parser.py +2 -2
  34. {cumulusci_plus-5.0.25.dist-info → cumulusci_plus-5.0.27.dist-info}/METADATA +6 -9
  35. {cumulusci_plus-5.0.25.dist-info → cumulusci_plus-5.0.27.dist-info}/RECORD +39 -33
  36. {cumulusci_plus-5.0.25.dist-info → cumulusci_plus-5.0.27.dist-info}/WHEEL +0 -0
  37. {cumulusci_plus-5.0.25.dist-info → cumulusci_plus-5.0.27.dist-info}/entry_points.txt +0 -0
  38. {cumulusci_plus-5.0.25.dist-info → cumulusci_plus-5.0.27.dist-info}/licenses/AUTHORS.rst +0 -0
  39. {cumulusci_plus-5.0.25.dist-info → cumulusci_plus-5.0.27.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,564 @@
1
+ """Tests for credentialManager module."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from unittest import mock
7
+
8
+ import pytest
9
+
10
+ from cumulusci.tasks.utility.credentialManager import (
11
+ AwsSecretsManagerProvider,
12
+ AzureVariableGroupProvider,
13
+ CredentialManager,
14
+ CredentialProvider,
15
+ DevEnvironmentVariableProvider,
16
+ EnvironmentVariableProvider,
17
+ )
18
+
19
+
20
+ class TestCredentialProvider:
21
+ """Test cases for CredentialProvider abstract base class."""
22
+
23
+ def test_init_with_key_prefix(self):
24
+ """Test initialization with custom key_prefix."""
25
+ provider = DevEnvironmentVariableProvider(key_prefix="CUSTOM_")
26
+ assert provider.key_prefix == "CUSTOM_"
27
+
28
+ @mock.patch.dict(os.environ, {"CUMULUSCI_PREFIX_SECRETS": "ENV_PREFIX_"})
29
+ def test_init_with_env_key_prefix(self):
30
+ """Test initialization with key_prefix from environment variable."""
31
+ provider = DevEnvironmentVariableProvider()
32
+ assert provider.key_prefix == "ENV_PREFIX_"
33
+
34
+ @mock.patch.dict(os.environ, {}, clear=True)
35
+ def test_init_without_key_prefix(self):
36
+ """Test initialization without key_prefix."""
37
+ provider = DevEnvironmentVariableProvider()
38
+ assert provider.key_prefix == ""
39
+
40
+ def test_get_key_with_prefix(self):
41
+ """Test get_key method with prefix."""
42
+ provider = DevEnvironmentVariableProvider(key_prefix="TEST_")
43
+ assert provider.get_key("API_KEY") == "TEST_API_KEY"
44
+
45
+ def test_get_key_without_prefix(self):
46
+ """Test get_key method without prefix."""
47
+ provider = DevEnvironmentVariableProvider(key_prefix="")
48
+ assert provider.get_key("API_KEY") == "API_KEY"
49
+
50
+ def test_provider_registration(self):
51
+ """Test that providers are registered in the registry."""
52
+ assert "local" in CredentialProvider._registry
53
+ assert "environment" in CredentialProvider._registry
54
+ assert "aws_secrets" in CredentialProvider._registry
55
+ assert "ado_variables" in CredentialProvider._registry
56
+
57
+ def test_abstract_methods_not_implemented(self):
58
+ """Test that abstract methods must be implemented by subclasses."""
59
+
60
+ class IncompleteProvider(CredentialProvider):
61
+ provider_type = "incomplete"
62
+
63
+ with pytest.raises(TypeError):
64
+ IncompleteProvider()
65
+
66
+
67
+ class TestDevEnvironmentVariableProvider:
68
+ """Test cases for DevEnvironmentVariableProvider."""
69
+
70
+ def test_provider_type(self):
71
+ """Test that provider_type is correctly set."""
72
+ assert DevEnvironmentVariableProvider.provider_type == "local"
73
+
74
+ def test_get_credentials_with_value(self):
75
+ """Test get_credentials returns the provided value."""
76
+ provider = DevEnvironmentVariableProvider()
77
+ result = provider.get_credentials("API_KEY", {"value": "secret123"})
78
+ assert result == "secret123"
79
+
80
+ def test_get_credentials_without_value(self):
81
+ """Test get_credentials returns None when no value is provided."""
82
+ provider = DevEnvironmentVariableProvider()
83
+ result = provider.get_credentials("API_KEY", {"value": None})
84
+ assert result is None
85
+
86
+ def test_get_credentials_with_empty_options(self):
87
+ """Test get_credentials with empty options dict."""
88
+ provider = DevEnvironmentVariableProvider()
89
+ result = provider.get_credentials("API_KEY", {})
90
+ assert result is None
91
+
92
+ def test_get_all_credentials_not_supported(self):
93
+ """Test that get_all_credentials raises NotImplementedError."""
94
+ provider = DevEnvironmentVariableProvider()
95
+ with pytest.raises(NotImplementedError):
96
+ provider.get_all_credentials("API_KEY", {})
97
+
98
+
99
+ class TestEnvironmentVariableProvider:
100
+ """Test cases for EnvironmentVariableProvider."""
101
+
102
+ def test_provider_type(self):
103
+ """Test that provider_type is correctly set."""
104
+ assert EnvironmentVariableProvider.provider_type == "environment"
105
+
106
+ @mock.patch.dict(os.environ, {"TEST_API_KEY": "env_secret"})
107
+ def test_get_credentials_from_environment(self):
108
+ """Test get_credentials retrieves value from environment."""
109
+ provider = EnvironmentVariableProvider(key_prefix="TEST_")
110
+ result = provider.get_credentials("API_KEY", {"value": "default"})
111
+ assert result == "env_secret"
112
+
113
+ @mock.patch.dict(os.environ, {}, clear=True)
114
+ def test_get_credentials_uses_default_when_env_not_set(self):
115
+ """Test get_credentials uses default value when env var not set."""
116
+ provider = EnvironmentVariableProvider(key_prefix="TEST_")
117
+ result = provider.get_credentials("API_KEY", {"value": "default_value"})
118
+ assert result == "default_value"
119
+
120
+ @mock.patch.dict(os.environ, {"MYAPP_KEY": "env_value"})
121
+ def test_get_credentials_with_custom_prefix(self):
122
+ """Test get_credentials with custom prefix."""
123
+ provider = EnvironmentVariableProvider(key_prefix="MYAPP_")
124
+ result = provider.get_credentials("KEY", {"value": "default"})
125
+ assert result == "env_value"
126
+
127
+ @mock.patch.dict(os.environ, {}, clear=True)
128
+ def test_get_credentials_returns_none_when_no_default(self):
129
+ """Test get_credentials returns None when no default and env not set."""
130
+ provider = EnvironmentVariableProvider()
131
+ result = provider.get_credentials("NONEXISTENT_KEY", {"value": None})
132
+ assert result is None
133
+
134
+ def test_get_all_credentials_not_supported(self):
135
+ """Test that get_all_credentials raises NotImplementedError."""
136
+ provider = EnvironmentVariableProvider()
137
+ with pytest.raises(NotImplementedError):
138
+ provider.get_all_credentials("API_KEY", {})
139
+
140
+
141
+ class TestAwsSecretsManagerProvider:
142
+ """Test cases for AwsSecretsManagerProvider."""
143
+
144
+ def test_provider_type(self):
145
+ """Test that provider_type is correctly set."""
146
+ assert AwsSecretsManagerProvider.provider_type == "aws_secrets"
147
+
148
+ @mock.patch.dict(os.environ, {"AWS_REGION": "us-east-1"})
149
+ def test_init_with_aws_region_from_env(self):
150
+ """Test initialization with AWS_REGION from environment."""
151
+ provider = AwsSecretsManagerProvider()
152
+ assert provider.aws_region == "us-east-1"
153
+ assert provider.secrets_cache == {}
154
+
155
+ def test_init_with_aws_region_from_kwargs(self):
156
+ """Test initialization with aws_region from kwargs."""
157
+ provider = AwsSecretsManagerProvider(aws_region="eu-west-1")
158
+ assert provider.aws_region == "eu-west-1"
159
+
160
+ @mock.patch.dict(os.environ, {}, clear=True)
161
+ def test_init_without_aws_region_raises_error(self):
162
+ """Test initialization without AWS_REGION raises ValueError."""
163
+ with pytest.raises(ValueError) as exc_info:
164
+ AwsSecretsManagerProvider()
165
+ assert "AWS_REGION" in str(exc_info.value)
166
+
167
+ def test_init_with_existing_secrets_cache(self):
168
+ """Test initialization with existing secrets_cache."""
169
+ cache = {"secret1": {"key1": "value1"}}
170
+ provider = AwsSecretsManagerProvider(
171
+ aws_region="us-west-2", secrets_cache=cache
172
+ )
173
+ assert provider.secrets_cache == cache
174
+
175
+ def test_get_credentials_success(self):
176
+ """Test get_credentials successfully retrieves secret."""
177
+ mock_client = mock.Mock()
178
+ mock_session = mock.Mock()
179
+ mock_session.client.return_value = mock_client
180
+ mock_boto3 = mock.Mock()
181
+ mock_boto3.session.Session.return_value = mock_session
182
+
183
+ secret_data = {"API_KEY": "secret_value", "DB_PASSWORD": "db_pass"}
184
+ mock_client.get_secret_value.return_value = {
185
+ "SecretString": json.dumps(secret_data)
186
+ }
187
+
188
+ with mock.patch.dict(
189
+ sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
190
+ ):
191
+ provider = AwsSecretsManagerProvider(aws_region="us-east-1")
192
+ result = provider.get_credentials(
193
+ "API_KEY", {"secret_name": "my-app/credentials"}
194
+ )
195
+
196
+ assert result == "secret_value"
197
+ mock_client.get_secret_value.assert_called_once_with(
198
+ SecretId="my-app/credentials"
199
+ )
200
+
201
+ def test_get_all_credentials_success(self):
202
+ """Test get_all_credentials retrieves all secrets."""
203
+ mock_client = mock.Mock()
204
+ mock_session = mock.Mock()
205
+ mock_session.client.return_value = mock_client
206
+ mock_boto3 = mock.Mock()
207
+ mock_boto3.session.Session.return_value = mock_session
208
+
209
+ secret_data = {"API_KEY": "secret_value", "DB_PASSWORD": "db_pass"}
210
+ mock_client.get_secret_value.return_value = {
211
+ "SecretString": json.dumps(secret_data)
212
+ }
213
+
214
+ with mock.patch.dict(
215
+ sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
216
+ ):
217
+ provider = AwsSecretsManagerProvider(aws_region="us-east-1")
218
+ result = provider.get_all_credentials(
219
+ "API_KEY", {"secret_name": "my-app/credentials"}
220
+ )
221
+
222
+ assert result == secret_data
223
+ assert "API_KEY" in result
224
+ assert "DB_PASSWORD" in result
225
+
226
+ def test_get_credentials_uses_cache(self):
227
+ """Test get_credentials uses cached secrets on subsequent calls."""
228
+ mock_client = mock.Mock()
229
+ mock_session = mock.Mock()
230
+ mock_session.client.return_value = mock_client
231
+ mock_boto3 = mock.Mock()
232
+ mock_boto3.session.Session.return_value = mock_session
233
+
234
+ secret_data = {"API_KEY": "secret_value"}
235
+ mock_client.get_secret_value.return_value = {
236
+ "SecretString": json.dumps(secret_data)
237
+ }
238
+
239
+ with mock.patch.dict(
240
+ sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
241
+ ):
242
+ provider = AwsSecretsManagerProvider(aws_region="us-east-1")
243
+
244
+ # First call - should hit AWS
245
+ result1 = provider.get_credentials(
246
+ "API_KEY", {"secret_name": "my-app/credentials"}
247
+ )
248
+ assert result1 == "secret_value"
249
+
250
+ # Second call - should use cache
251
+ result2 = provider.get_credentials(
252
+ "API_KEY", {"secret_name": "my-app/credentials"}
253
+ )
254
+ assert result2 == "secret_value"
255
+
256
+ # Should only call AWS once
257
+ assert mock_client.get_secret_value.call_count == 1
258
+
259
+ def test_get_credentials_without_secret_name_raises_error(self):
260
+ """Test get_credentials raises error when secret_name is not provided."""
261
+ provider = AwsSecretsManagerProvider(aws_region="us-east-1")
262
+ with pytest.raises(ValueError) as exc_info:
263
+ provider.get_credentials("API_KEY", {})
264
+ assert "Secret name is required" in str(exc_info.value)
265
+
266
+ def test_get_credentials_handles_client_error(self):
267
+ """Test get_credentials handles boto3 ClientError."""
268
+ # We need to import ClientError first so it exists in the namespace
269
+ try:
270
+ from botocore.exceptions import ClientError
271
+ except ImportError:
272
+ # If botocore isn't installed, skip this test
273
+ pytest.skip("botocore not installed")
274
+
275
+ mock_client = mock.Mock()
276
+ mock_session = mock.Mock()
277
+ mock_session.client.return_value = mock_client
278
+ mock_boto3 = mock.Mock()
279
+ mock_boto3.session.Session.return_value = mock_session
280
+
281
+ error_response = {"Error": {"Code": "ResourceNotFoundException"}}
282
+ mock_client.get_secret_value.side_effect = ClientError(
283
+ error_response, "GetSecretValue"
284
+ )
285
+
286
+ # Create a proper mock for botocore.exceptions
287
+ mock_botocore_exceptions = type(sys)("botocore.exceptions")
288
+ mock_botocore_exceptions.ClientError = ClientError
289
+
290
+ with mock.patch.dict(
291
+ sys.modules,
292
+ {"boto3": mock_boto3, "botocore.exceptions": mock_botocore_exceptions},
293
+ ):
294
+ provider = AwsSecretsManagerProvider(aws_region="us-east-1")
295
+
296
+ with pytest.raises(ClientError):
297
+ provider.get_credentials("API_KEY", {"secret_name": "nonexistent"})
298
+
299
+ def test_get_credentials_handles_import_error(self):
300
+ """Test get_credentials handles ImportError when boto3 is not installed."""
301
+ # Create a module that raises ImportError when boto3 is accessed
302
+ original_import = __builtins__["__import__"]
303
+
304
+ def mock_import(name, *args, **kwargs):
305
+ if name == "boto3" or name == "botocore":
306
+ raise ImportError(f"No module named '{name}'")
307
+ return original_import(name, *args, **kwargs)
308
+
309
+ provider = AwsSecretsManagerProvider(aws_region="us-east-1")
310
+
311
+ with mock.patch("builtins.__import__", side_effect=mock_import):
312
+ # When boto3 import fails, we get an UnboundLocalError because ClientError
313
+ # can't be imported either. The code catches this with a RuntimeError.
314
+ with pytest.raises((RuntimeError, UnboundLocalError)):
315
+ provider.get_credentials("API_KEY", {"secret_name": "my-secret"})
316
+
317
+ def test_get_credentials_handles_general_exception(self):
318
+ """Test get_credentials handles general exceptions."""
319
+ # We need to import ClientError first so exception handling works properly
320
+ try:
321
+ from botocore.exceptions import ClientError
322
+ except ImportError:
323
+ pytest.skip("botocore not installed")
324
+
325
+ mock_client = mock.Mock()
326
+ mock_session = mock.Mock()
327
+ mock_session.client.return_value = mock_client
328
+ mock_boto3 = mock.Mock()
329
+ mock_boto3.session.Session.return_value = mock_session
330
+
331
+ mock_client.get_secret_value.side_effect = Exception("Unexpected error")
332
+
333
+ # Create a proper mock for botocore.exceptions
334
+ mock_botocore_exceptions = type(sys)("botocore.exceptions")
335
+ mock_botocore_exceptions.ClientError = ClientError
336
+
337
+ with mock.patch.dict(
338
+ sys.modules,
339
+ {"boto3": mock_boto3, "botocore.exceptions": mock_botocore_exceptions},
340
+ ):
341
+ provider = AwsSecretsManagerProvider(aws_region="us-east-1")
342
+
343
+ with pytest.raises(RuntimeError) as exc_info:
344
+ provider.get_credentials("API_KEY", {"secret_name": "my-secret"})
345
+ assert "Failed to retrieve secret" in str(exc_info.value)
346
+
347
+ def test_get_credentials_returns_none_for_missing_key(self):
348
+ """Test get_credentials returns None when key is not in secret."""
349
+ mock_client = mock.Mock()
350
+ mock_session = mock.Mock()
351
+ mock_session.client.return_value = mock_client
352
+ mock_boto3 = mock.Mock()
353
+ mock_boto3.session.Session.return_value = mock_session
354
+
355
+ secret_data = {"OTHER_KEY": "other_value"}
356
+ mock_client.get_secret_value.return_value = {
357
+ "SecretString": json.dumps(secret_data)
358
+ }
359
+
360
+ with mock.patch.dict(
361
+ sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
362
+ ):
363
+ provider = AwsSecretsManagerProvider(aws_region="us-east-1")
364
+ result = provider.get_credentials(
365
+ "MISSING_KEY", {"secret_name": "my-app/credentials"}
366
+ )
367
+
368
+ assert result is None
369
+
370
+
371
+ class TestAzureVariableGroupProvider:
372
+ """Test cases for AzureVariableGroupProvider."""
373
+
374
+ def test_provider_type(self):
375
+ """Test that provider_type is correctly set."""
376
+ assert AzureVariableGroupProvider.provider_type == "ado_variables"
377
+
378
+ @mock.patch.dict(os.environ, {"MYAPP_API_KEY": "azure_secret"})
379
+ def test_get_credentials_from_azure_variables(self):
380
+ """Test get_credentials retrieves value from Azure variable group."""
381
+ provider = AzureVariableGroupProvider(key_prefix="MYAPP_")
382
+ result = provider.get_credentials("API_KEY", {})
383
+ assert result == "azure_secret"
384
+
385
+ @mock.patch.dict(os.environ, {"MYAPP_API_KEY": "azure_secret"})
386
+ def test_get_credentials_handles_dots_in_key(self):
387
+ """Test get_credentials converts dots to underscores."""
388
+ provider = AzureVariableGroupProvider(key_prefix="MYAPP_")
389
+ result = provider.get_credentials("API.KEY", {})
390
+ assert result == "azure_secret"
391
+
392
+ @mock.patch.dict(os.environ, {"MYAPP_API_KEY": "uppercase_secret"})
393
+ def test_get_credentials_handles_case_insensitive(self):
394
+ """Test get_credentials handles uppercase conversion."""
395
+ provider = AzureVariableGroupProvider(key_prefix="myapp_")
396
+ result = provider.get_credentials("api_key", {})
397
+ assert result == "uppercase_secret"
398
+
399
+ @mock.patch.dict(os.environ, {}, clear=True)
400
+ def test_get_credentials_returns_none_when_not_found(self):
401
+ """Test get_credentials returns None when variable is not found."""
402
+ provider = AzureVariableGroupProvider(key_prefix="MYAPP_")
403
+ result = provider.get_credentials("NONEXISTENT_KEY", {})
404
+ assert result is None
405
+
406
+ @mock.patch.dict(os.environ, {"PREFIX_MY_VAR_NAME": "value123"})
407
+ def test_get_credentials_with_complex_key(self):
408
+ """Test get_credentials with complex key name."""
409
+ provider = AzureVariableGroupProvider(key_prefix="PREFIX_")
410
+ result = provider.get_credentials("my.var.name", {})
411
+ assert result == "value123"
412
+
413
+ def test_get_all_credentials_not_supported(self):
414
+ """Test that get_all_credentials raises NotImplementedError."""
415
+ provider = AzureVariableGroupProvider()
416
+ with pytest.raises(NotImplementedError):
417
+ provider.get_all_credentials("API_KEY", {})
418
+
419
+
420
+ class TestCredentialManager:
421
+ """Test cases for CredentialManager."""
422
+
423
+ def test_env_secrets_type_constant(self):
424
+ """Test that env_secrets_type constant is correctly set."""
425
+ assert CredentialManager.env_secrets_type == "CUMULUSCI_SECRETS_TYPE"
426
+
427
+ @mock.patch.dict(os.environ, {"CUMULUSCI_SECRETS_TYPE": "local"})
428
+ def test_load_secrets_type_from_environment(self):
429
+ """Test loading secrets type from environment variable."""
430
+ provider_type = CredentialManager.load_secrets_type_from_environment()
431
+ assert provider_type == "local"
432
+
433
+ @mock.patch.dict(os.environ, {"CUMULUSCI_SECRETS_TYPE": "AWS_SECRETS"})
434
+ def test_load_secrets_type_case_insensitive(self):
435
+ """Test loading secrets type is case insensitive."""
436
+ provider_type = CredentialManager.load_secrets_type_from_environment()
437
+ assert provider_type == "aws_secrets"
438
+
439
+ @mock.patch.dict(os.environ, {}, clear=True)
440
+ def test_load_secrets_type_defaults_to_local(self):
441
+ """Test loading secrets type defaults to 'local' when not set."""
442
+ provider_type = CredentialManager.load_secrets_type_from_environment()
443
+ assert provider_type == "local"
444
+
445
+ @mock.patch.dict(os.environ, {"CUMULUSCI_SECRETS_TYPE": "environment"})
446
+ def test_get_provider_from_environment(self):
447
+ """Test get_provider loads provider type from environment."""
448
+ provider = CredentialManager.get_provider()
449
+ assert isinstance(provider, EnvironmentVariableProvider)
450
+ assert provider.provider_type == "environment"
451
+
452
+ def test_get_provider_with_explicit_type(self):
453
+ """Test get_provider with explicit provider type."""
454
+ provider = CredentialManager.get_provider(provider_type="local")
455
+ assert isinstance(provider, DevEnvironmentVariableProvider)
456
+ assert provider.provider_type == "local"
457
+
458
+ def test_get_provider_with_kwargs(self):
459
+ """Test get_provider passes kwargs to provider constructor."""
460
+ provider = CredentialManager.get_provider(
461
+ provider_type="environment", key_prefix="CUSTOM_"
462
+ )
463
+ assert isinstance(provider, EnvironmentVariableProvider)
464
+ assert provider.key_prefix == "CUSTOM_"
465
+
466
+ def test_get_provider_with_aws_secrets(self):
467
+ """Test get_provider with AWS Secrets Manager."""
468
+ provider = CredentialManager.get_provider(
469
+ provider_type="aws_secrets", aws_region="us-west-2"
470
+ )
471
+ assert isinstance(provider, AwsSecretsManagerProvider)
472
+ assert provider.aws_region == "us-west-2"
473
+
474
+ def test_get_provider_with_ado_variables(self):
475
+ """Test get_provider with Azure DevOps variables."""
476
+ provider = CredentialManager.get_provider(provider_type="ado_variables")
477
+ assert isinstance(provider, AzureVariableGroupProvider)
478
+ assert provider.provider_type == "ado_variables"
479
+
480
+ def test_get_provider_with_invalid_type_raises_error(self):
481
+ """Test get_provider raises error for invalid provider type."""
482
+ with pytest.raises(ValueError) as exc_info:
483
+ CredentialManager.get_provider(provider_type="invalid_provider")
484
+ assert "Unknown provider type specified" in str(exc_info.value)
485
+ assert "invalid_provider" in str(exc_info.value)
486
+
487
+ @mock.patch.dict(os.environ, {"CUMULUSCI_SECRETS_TYPE": "invalid"})
488
+ def test_get_provider_raises_error_for_invalid_env_type(self):
489
+ """Test get_provider raises error when env var has invalid type."""
490
+ with pytest.raises(ValueError) as exc_info:
491
+ CredentialManager.get_provider()
492
+ assert "Unknown provider type specified" in str(exc_info.value)
493
+
494
+ @mock.patch.dict(os.environ, {"CUMULUSCI_SECRETS_TYPE": "local"})
495
+ def test_get_provider_logs_provider_type(self):
496
+ """Test get_provider logs the provider type being used."""
497
+ with mock.patch("logging.Logger.info"):
498
+ provider = CredentialManager.get_provider()
499
+ # The logger should have been called with info about the provider
500
+ assert provider is not None
501
+
502
+
503
+ class TestProviderIntegration:
504
+ """Integration tests for provider workflow."""
505
+
506
+ @mock.patch.dict(
507
+ os.environ,
508
+ {
509
+ "CUMULUSCI_SECRETS_TYPE": "environment",
510
+ "MYAPP_DATABASE_URL": "postgres://localhost/mydb",
511
+ },
512
+ )
513
+ def test_full_workflow_environment_provider(self):
514
+ """Test complete workflow using environment provider."""
515
+ provider = CredentialManager.get_provider(key_prefix="MYAPP_")
516
+ credentials = provider.get_credentials(
517
+ "DATABASE_URL", {"value": "default_db_url"}
518
+ )
519
+ assert credentials == "postgres://localhost/mydb"
520
+
521
+ @mock.patch.dict(os.environ, {"CUMULUSCI_SECRETS_TYPE": "local"})
522
+ def test_full_workflow_local_provider(self):
523
+ """Test complete workflow using local provider."""
524
+ provider = CredentialManager.get_provider()
525
+ credentials = provider.get_credentials(
526
+ "API_KEY", {"value": "local_api_key_123"}
527
+ )
528
+ assert credentials == "local_api_key_123"
529
+
530
+ @mock.patch.dict(os.environ, {"CUMULUSCI_SECRETS_TYPE": "aws_secrets"})
531
+ def test_full_workflow_aws_provider(self):
532
+ """Test complete workflow using AWS Secrets Manager provider."""
533
+ mock_client = mock.Mock()
534
+ mock_session = mock.Mock()
535
+ mock_session.client.return_value = mock_client
536
+ mock_boto3 = mock.Mock()
537
+ mock_boto3.session.Session.return_value = mock_session
538
+
539
+ secret_data = {"API_KEY": "aws_secret_123"}
540
+ mock_client.get_secret_value.return_value = {
541
+ "SecretString": json.dumps(secret_data)
542
+ }
543
+
544
+ with mock.patch.dict(
545
+ sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
546
+ ):
547
+ provider = CredentialManager.get_provider(aws_region="us-east-1")
548
+ credentials = provider.get_credentials(
549
+ "API_KEY", {"secret_name": "my-app/prod"}
550
+ )
551
+ assert credentials == "aws_secret_123"
552
+
553
+ @mock.patch.dict(
554
+ os.environ,
555
+ {
556
+ "CUMULUSCI_SECRETS_TYPE": "ado_variables",
557
+ "MYAPP_API_TOKEN": "ado_token_xyz",
558
+ },
559
+ )
560
+ def test_full_workflow_ado_provider(self):
561
+ """Test complete workflow using Azure DevOps variables provider."""
562
+ provider = CredentialManager.get_provider(key_prefix="MYAPP_")
563
+ credentials = provider.get_credentials("API_TOKEN", {})
564
+ assert credentials == "ado_token_xyz"