cumulusci-plus 5.0.24__py3-none-any.whl → 5.0.26__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.
- cumulusci/__about__.py +1 -1
- cumulusci/cli/task.py +17 -0
- cumulusci/cli/tests/test_error.py +3 -1
- cumulusci/cli/tests/test_task.py +88 -2
- cumulusci/core/github.py +1 -1
- cumulusci/core/sfdx.py +3 -1
- cumulusci/cumulusci.yml +20 -0
- cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
- cumulusci/salesforce_api/rest_deploy.py +1 -1
- cumulusci/tasks/apex/anon.py +1 -1
- cumulusci/tasks/apex/testrunner.py +6 -1
- cumulusci/tasks/bulkdata/extract.py +0 -1
- cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
- cumulusci/tasks/bulkdata/tests/test_select_utils.py +6 -0
- cumulusci/tasks/metadata_etl/base.py +7 -3
- cumulusci/tasks/push/README.md +15 -17
- cumulusci/tasks/release_notes/README.md +13 -13
- cumulusci/tasks/robotframework/tests/test_robotframework.py +1 -1
- cumulusci/tasks/salesforce/Deploy.py +5 -1
- cumulusci/tasks/salesforce/composite.py +1 -1
- cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
- cumulusci/tasks/salesforce/enable_prediction.py +5 -1
- cumulusci/tasks/salesforce/sourcetracking.py +1 -1
- cumulusci/tasks/salesforce/tests/test_update_external_credential.py +912 -0
- cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
- cumulusci/tasks/salesforce/update_external_credential.py +562 -0
- cumulusci/tasks/salesforce/update_named_credential.py +441 -0
- cumulusci/tasks/salesforce/update_profile.py +17 -13
- cumulusci/tasks/salesforce/users/permsets.py +70 -2
- cumulusci/tasks/salesforce/users/tests/test_permsets.py +184 -0
- cumulusci/tasks/sfdmu/__init__.py +0 -0
- cumulusci/tasks/sfdmu/sfdmu.py +256 -0
- cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
- cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
- cumulusci/tasks/sfdmu/tests/test_sfdmu.py +443 -0
- cumulusci/tasks/utility/credentialManager.py +256 -0
- cumulusci/tasks/utility/directoryRecreator.py +30 -0
- cumulusci/tasks/utility/secretsToEnv.py +130 -0
- cumulusci/tasks/utility/tests/test_credentialManager.py +564 -0
- cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
- cumulusci/tasks/utility/tests/test_secretsToEnv.py +1091 -0
- cumulusci/utils/__init__.py +23 -1
- cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
- cumulusci/utils/yaml/tests/test_model_parser.py +2 -2
- {cumulusci_plus-5.0.24.dist-info → cumulusci_plus-5.0.26.dist-info}/METADATA +7 -9
- {cumulusci_plus-5.0.24.dist-info → cumulusci_plus-5.0.26.dist-info}/RECORD +50 -35
- {cumulusci_plus-5.0.24.dist-info → cumulusci_plus-5.0.26.dist-info}/WHEEL +0 -0
- {cumulusci_plus-5.0.24.dist-info → cumulusci_plus-5.0.26.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.24.dist-info → cumulusci_plus-5.0.26.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.24.dist-info → cumulusci_plus-5.0.26.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"
|