cumulusci-plus 5.0.21__py3-none-any.whl → 5.0.35__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/logger.py +2 -2
- cumulusci/cli/service.py +20 -0
- cumulusci/cli/task.py +17 -0
- cumulusci/cli/tests/test_error.py +3 -1
- cumulusci/cli/tests/test_flow.py +279 -2
- cumulusci/cli/tests/test_service.py +15 -12
- cumulusci/cli/tests/test_task.py +88 -2
- cumulusci/cli/tests/utils.py +1 -4
- cumulusci/core/config/base_task_flow_config.py +26 -1
- cumulusci/core/config/project_config.py +2 -20
- cumulusci/core/config/tests/test_config_expensive.py +9 -3
- cumulusci/core/config/universal_config.py +3 -4
- cumulusci/core/dependencies/base.py +1 -1
- cumulusci/core/dependencies/dependencies.py +1 -1
- cumulusci/core/dependencies/github.py +1 -2
- cumulusci/core/dependencies/resolvers.py +1 -1
- cumulusci/core/dependencies/tests/test_dependencies.py +1 -1
- cumulusci/core/dependencies/tests/test_resolvers.py +1 -1
- cumulusci/core/flowrunner.py +90 -6
- cumulusci/core/github.py +1 -1
- cumulusci/core/sfdx.py +3 -1
- cumulusci/core/source_transforms/tests/test_transforms.py +1 -1
- cumulusci/core/source_transforms/transforms.py +1 -1
- cumulusci/core/tasks.py +13 -2
- cumulusci/core/tests/test_flowrunner.py +100 -0
- cumulusci/core/tests/test_tasks.py +65 -0
- cumulusci/core/utils.py +3 -1
- cumulusci/core/versions.py +1 -1
- cumulusci/cumulusci.yml +55 -0
- cumulusci/oauth/client.py +1 -1
- cumulusci/plugins/plugin_base.py +5 -3
- cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
- cumulusci/salesforce_api/rest_deploy.py +1 -1
- cumulusci/schema/cumulusci.jsonschema.json +64 -0
- cumulusci/tasks/apex/anon.py +1 -1
- cumulusci/tasks/apex/testrunner.py +416 -142
- cumulusci/tasks/apex/tests/test_apex_tasks.py +917 -1
- cumulusci/tasks/bulkdata/extract.py +0 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +1 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +1 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +1 -1
- cumulusci/tasks/bulkdata/generate_and_load_data.py +136 -12
- cumulusci/tasks/bulkdata/mapping_parser.py +139 -44
- cumulusci/tasks/bulkdata/select_utils.py +1 -1
- cumulusci/tasks/bulkdata/snowfakery.py +100 -25
- cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +159 -0
- cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
- cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +763 -1
- cumulusci/tasks/bulkdata/tests/test_select_utils.py +26 -0
- cumulusci/tasks/bulkdata/tests/test_snowfakery.py +133 -0
- cumulusci/tasks/create_package_version.py +190 -16
- cumulusci/tasks/datadictionary.py +1 -1
- cumulusci/tasks/metadata_etl/base.py +7 -3
- cumulusci/tasks/metadata_etl/layouts.py +1 -1
- cumulusci/tasks/metadata_etl/permissions.py +1 -1
- cumulusci/tasks/metadata_etl/remote_site_settings.py +2 -2
- cumulusci/tasks/push/README.md +15 -17
- cumulusci/tasks/release_notes/README.md +13 -13
- cumulusci/tasks/release_notes/generator.py +13 -8
- cumulusci/tasks/robotframework/tests/test_robotframework.py +6 -1
- cumulusci/tasks/salesforce/Deploy.py +53 -2
- cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
- cumulusci/tasks/salesforce/__init__.py +1 -0
- cumulusci/tasks/salesforce/assign_ps_psg.py +448 -0
- 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/getPackageVersion.py +89 -0
- cumulusci/tasks/salesforce/sourcetracking.py +1 -1
- cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
- cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
- cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
- cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
- cumulusci/tasks/salesforce/tests/test_update_dependencies.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_dependencies.py +2 -2
- 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 +62 -5
- cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
- cumulusci/tasks/sfdmu/__init__.py +0 -0
- cumulusci/tasks/sfdmu/sfdmu.py +363 -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 +1012 -0
- cumulusci/tasks/tests/test_create_package_version.py +716 -1
- cumulusci/tasks/tests/test_util.py +42 -0
- cumulusci/tasks/util.py +37 -1
- cumulusci/tasks/utility/copyContents.py +402 -0
- cumulusci/tasks/utility/credentialManager.py +256 -0
- cumulusci/tasks/utility/directoryRecreator.py +30 -0
- cumulusci/tasks/utility/env_management.py +1 -1
- cumulusci/tasks/utility/secretsToEnv.py +135 -0
- cumulusci/tasks/utility/tests/test_copyContents.py +1719 -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/tests/test_integration_infrastructure.py +3 -1
- cumulusci/tests/test_utils.py +70 -6
- cumulusci/utils/__init__.py +54 -9
- cumulusci/utils/classutils.py +5 -2
- cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
- cumulusci/utils/options.py +23 -1
- cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
- cumulusci/utils/yaml/cumulusci_yml.py +7 -3
- cumulusci/utils/yaml/model_parser.py +2 -2
- cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
- cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
- cumulusci/vcs/base.py +23 -15
- cumulusci/vcs/bootstrap.py +5 -4
- cumulusci/vcs/utils/list_modified_files.py +189 -0
- cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/METADATA +12 -10
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/RECORD +121 -96
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/WHEEL +0 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.35.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
"""Tests for secretsToEnv module."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from unittest import mock
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from cumulusci.core.config import TaskConfig
|
|
11
|
+
from cumulusci.tasks.utility.credentialManager import DevEnvironmentVariableProvider
|
|
12
|
+
from cumulusci.tasks.utility.secretsToEnv import SecretsToEnv
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestSecretsToEnvOptions:
|
|
16
|
+
"""Test cases for SecretsToEnv options configuration."""
|
|
17
|
+
|
|
18
|
+
def test_default_options(self):
|
|
19
|
+
"""Test initialization with default options."""
|
|
20
|
+
task_config = TaskConfig({"options": {}})
|
|
21
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
22
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
23
|
+
task_config.options["env_path"] = env_path
|
|
24
|
+
|
|
25
|
+
task = SecretsToEnv(
|
|
26
|
+
project_config=mock.Mock(),
|
|
27
|
+
task_config=task_config,
|
|
28
|
+
org_config=None,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
assert task.parsed_options.env_path == Path(env_path)
|
|
32
|
+
assert task.parsed_options.secrets_provider is None
|
|
33
|
+
assert task.parsed_options.provider_options == {}
|
|
34
|
+
assert task.parsed_options.secrets == []
|
|
35
|
+
|
|
36
|
+
def test_custom_options(self):
|
|
37
|
+
"""Test initialization with custom options."""
|
|
38
|
+
task_config = TaskConfig(
|
|
39
|
+
{
|
|
40
|
+
"options": {
|
|
41
|
+
"env_path": ".custom.env",
|
|
42
|
+
"secrets_provider": "environment",
|
|
43
|
+
"provider_options": {"key_prefix": "CUSTOM_"},
|
|
44
|
+
"secrets": ["API_KEY", "DB_PASSWORD"],
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
task = SecretsToEnv(
|
|
50
|
+
project_config=mock.Mock(),
|
|
51
|
+
task_config=task_config,
|
|
52
|
+
org_config=None,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
assert task.parsed_options.env_path == Path(".custom.env")
|
|
56
|
+
assert task.parsed_options.secrets_provider == "environment"
|
|
57
|
+
assert task.parsed_options.provider_options == {"key_prefix": "CUSTOM_"}
|
|
58
|
+
assert task.parsed_options.secrets == ["API_KEY", "DB_PASSWORD"]
|
|
59
|
+
|
|
60
|
+
def test_secrets_as_mapping(self):
|
|
61
|
+
"""Test initialization with secrets as mapping."""
|
|
62
|
+
task_config = TaskConfig(
|
|
63
|
+
{
|
|
64
|
+
"options": {
|
|
65
|
+
"env_path": ".env",
|
|
66
|
+
"secrets": {"DB_URL": "database_url", "API_KEY": "api_key"},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
task = SecretsToEnv(
|
|
72
|
+
project_config=mock.Mock(),
|
|
73
|
+
task_config=task_config,
|
|
74
|
+
org_config=None,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
assert task.parsed_options.secrets == {
|
|
78
|
+
"DB_URL": "database_url",
|
|
79
|
+
"API_KEY": "api_key",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TestSecretsToEnvInitialization:
|
|
84
|
+
"""Test cases for SecretsToEnv initialization methods."""
|
|
85
|
+
|
|
86
|
+
def test_init_options_creates_provider(self):
|
|
87
|
+
"""Test that _init_options creates the correct provider."""
|
|
88
|
+
task_config = TaskConfig(
|
|
89
|
+
{
|
|
90
|
+
"options": {
|
|
91
|
+
"env_path": ".env",
|
|
92
|
+
"secrets_provider": "local",
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
task = SecretsToEnv(
|
|
98
|
+
project_config=mock.Mock(),
|
|
99
|
+
task_config=task_config,
|
|
100
|
+
org_config=None,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
assert task.provider is not None
|
|
104
|
+
assert isinstance(task.provider, DevEnvironmentVariableProvider)
|
|
105
|
+
|
|
106
|
+
def test_init_options_loads_existing_env_file(self):
|
|
107
|
+
"""Test that _init_options loads existing .env file."""
|
|
108
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
109
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
110
|
+
|
|
111
|
+
# Create existing .env file
|
|
112
|
+
with open(env_path, "w") as f:
|
|
113
|
+
f.write('EXISTING_KEY="existing_value"\n')
|
|
114
|
+
|
|
115
|
+
task_config = TaskConfig({"options": {"env_path": env_path}})
|
|
116
|
+
|
|
117
|
+
task = SecretsToEnv(
|
|
118
|
+
project_config=mock.Mock(),
|
|
119
|
+
task_config=task_config,
|
|
120
|
+
org_config=None,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
assert "EXISTING_KEY" in task.env_values
|
|
124
|
+
assert task.env_values["EXISTING_KEY"] == "existing_value"
|
|
125
|
+
|
|
126
|
+
def test_init_secrets_with_list_of_strings(self):
|
|
127
|
+
"""Test _init_secrets with list of strings."""
|
|
128
|
+
task_config = TaskConfig(
|
|
129
|
+
{
|
|
130
|
+
"options": {
|
|
131
|
+
"env_path": ".env",
|
|
132
|
+
"secrets": ["API_KEY", "DB_PASSWORD", "TOKEN"],
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
task = SecretsToEnv(
|
|
138
|
+
project_config=mock.Mock(),
|
|
139
|
+
task_config=task_config,
|
|
140
|
+
org_config=None,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
task._init_secrets()
|
|
144
|
+
|
|
145
|
+
# Should convert list to mapping with same key and value
|
|
146
|
+
assert task.secrets == {
|
|
147
|
+
"API_KEY": "API_KEY",
|
|
148
|
+
"DB_PASSWORD": "DB_PASSWORD",
|
|
149
|
+
"TOKEN": "TOKEN",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
def test_init_secrets_with_mapping_format_in_list(self):
|
|
153
|
+
"""Test _init_secrets with mapping format in list (key:value)."""
|
|
154
|
+
task_config = TaskConfig(
|
|
155
|
+
{
|
|
156
|
+
"options": {
|
|
157
|
+
"env_path": ".env",
|
|
158
|
+
"secrets": ["DB_URL:database_url", "API_KEY:api_key"],
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
task = SecretsToEnv(
|
|
164
|
+
project_config=mock.Mock(),
|
|
165
|
+
task_config=task_config,
|
|
166
|
+
org_config=None,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
task._init_secrets()
|
|
170
|
+
|
|
171
|
+
# Should parse as mapping
|
|
172
|
+
assert task.secrets == {
|
|
173
|
+
"DB_URL": "database_url",
|
|
174
|
+
"API_KEY": "api_key",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def test_init_secrets_with_empty_list(self):
|
|
178
|
+
"""Test _init_secrets with empty list doesn't initialize secrets."""
|
|
179
|
+
task_config = TaskConfig(
|
|
180
|
+
{
|
|
181
|
+
"options": {
|
|
182
|
+
"env_path": ".env",
|
|
183
|
+
"secrets": [],
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
task = SecretsToEnv(
|
|
189
|
+
project_config=mock.Mock(),
|
|
190
|
+
task_config=task_config,
|
|
191
|
+
org_config=None,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
task._init_secrets()
|
|
195
|
+
|
|
196
|
+
# Should not set secrets attribute if list is empty
|
|
197
|
+
assert task.secrets == {}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class TestSecretsToEnvGetCredential:
|
|
201
|
+
"""Test cases for _get_credential method."""
|
|
202
|
+
|
|
203
|
+
def test_get_credential_success(self):
|
|
204
|
+
"""Test _get_credential successfully retrieves credential."""
|
|
205
|
+
task_config = TaskConfig({"options": {"env_path": ".env"}})
|
|
206
|
+
|
|
207
|
+
task = SecretsToEnv(
|
|
208
|
+
project_config=mock.Mock(),
|
|
209
|
+
task_config=task_config,
|
|
210
|
+
org_config=None,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
mock_provider = mock.Mock()
|
|
214
|
+
mock_provider.provider_type = "local"
|
|
215
|
+
mock_provider.get_credentials.return_value = "secret_value_123"
|
|
216
|
+
task.provider = mock_provider
|
|
217
|
+
|
|
218
|
+
safe_value, original_value = task._get_credential(
|
|
219
|
+
"API_KEY", "api_key", secret_name="my-secret"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
assert safe_value == "secret_value_123"
|
|
223
|
+
assert original_value == "secret_value_123"
|
|
224
|
+
mock_provider.get_credentials.assert_called_once_with(
|
|
225
|
+
"API_KEY", {"value": "api_key", "secret_name": "my-secret"}
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def test_get_credential_escapes_quotes(self):
|
|
229
|
+
"""Test _get_credential escapes double quotes."""
|
|
230
|
+
task_config = TaskConfig({"options": {"env_path": ".env"}})
|
|
231
|
+
|
|
232
|
+
task = SecretsToEnv(
|
|
233
|
+
project_config=mock.Mock(),
|
|
234
|
+
task_config=task_config,
|
|
235
|
+
org_config=None,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
mock_provider = mock.Mock()
|
|
239
|
+
mock_provider.provider_type = "local"
|
|
240
|
+
mock_provider.get_credentials.return_value = 'value_with_"quotes"'
|
|
241
|
+
task.provider = mock_provider
|
|
242
|
+
|
|
243
|
+
safe_value, _ = task._get_credential("API_KEY", "api_key")
|
|
244
|
+
|
|
245
|
+
assert safe_value == 'value_with_\\"quotes\\"'
|
|
246
|
+
|
|
247
|
+
def test_get_credential_escapes_newlines(self):
|
|
248
|
+
"""Test _get_credential escapes newlines."""
|
|
249
|
+
task_config = TaskConfig({"options": {"env_path": ".env"}})
|
|
250
|
+
|
|
251
|
+
task = SecretsToEnv(
|
|
252
|
+
project_config=mock.Mock(),
|
|
253
|
+
task_config=task_config,
|
|
254
|
+
org_config=None,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
mock_provider = mock.Mock()
|
|
258
|
+
mock_provider.provider_type = "local"
|
|
259
|
+
mock_provider.get_credentials.return_value = "line1\nline2\nline3"
|
|
260
|
+
task.provider = mock_provider
|
|
261
|
+
|
|
262
|
+
safe_value, _ = task._get_credential("API_KEY", "api_key")
|
|
263
|
+
|
|
264
|
+
assert safe_value == "line1\\nline2\\nline3"
|
|
265
|
+
|
|
266
|
+
def test_get_credential_handles_both_quotes_and_newlines(self):
|
|
267
|
+
"""Test _get_credential handles both quotes and newlines."""
|
|
268
|
+
task_config = TaskConfig({"options": {"env_path": ".env"}})
|
|
269
|
+
|
|
270
|
+
task = SecretsToEnv(
|
|
271
|
+
project_config=mock.Mock(),
|
|
272
|
+
task_config=task_config,
|
|
273
|
+
org_config=None,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
mock_provider = mock.Mock()
|
|
277
|
+
mock_provider.provider_type = "local"
|
|
278
|
+
mock_provider.get_credentials.return_value = 'line1 "quoted"\nline2'
|
|
279
|
+
task.provider = mock_provider
|
|
280
|
+
|
|
281
|
+
safe_value, _ = task._get_credential("API_KEY", "api_key")
|
|
282
|
+
|
|
283
|
+
assert safe_value == 'line1 \\"quoted\\"\\nline2'
|
|
284
|
+
|
|
285
|
+
def test_get_credential_with_none_value_raises_error(self):
|
|
286
|
+
"""Test _get_credential raises error when provider returns None."""
|
|
287
|
+
task_config = TaskConfig({"options": {"env_path": ".env"}})
|
|
288
|
+
|
|
289
|
+
task = SecretsToEnv(
|
|
290
|
+
project_config=mock.Mock(),
|
|
291
|
+
task_config=task_config,
|
|
292
|
+
org_config=None,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
mock_provider = mock.Mock()
|
|
296
|
+
mock_provider.provider_type = "local"
|
|
297
|
+
mock_provider.get_credentials.return_value = None
|
|
298
|
+
task.provider = mock_provider
|
|
299
|
+
|
|
300
|
+
with pytest.raises(ValueError) as exc_info:
|
|
301
|
+
task._get_credential("API_KEY", "api_key")
|
|
302
|
+
|
|
303
|
+
assert "Failed to retrieve secret API_KEY from local" in str(exc_info.value)
|
|
304
|
+
|
|
305
|
+
def test_get_credential_uses_env_key_parameter(self):
|
|
306
|
+
"""Test _get_credential uses custom env_key when provided."""
|
|
307
|
+
task_config = TaskConfig({"options": {"env_path": ".env"}})
|
|
308
|
+
|
|
309
|
+
task = SecretsToEnv(
|
|
310
|
+
project_config=mock.Mock(),
|
|
311
|
+
task_config=task_config,
|
|
312
|
+
org_config=None,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
mock_provider = mock.Mock()
|
|
316
|
+
mock_provider.provider_type = "local"
|
|
317
|
+
mock_provider.get_credentials.return_value = "secret_value"
|
|
318
|
+
task.provider = mock_provider
|
|
319
|
+
|
|
320
|
+
safe_value, _ = task._get_credential(
|
|
321
|
+
"CREDENTIAL_KEY", "value", env_key="CUSTOM_ENV_KEY"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
assert safe_value == "secret_value"
|
|
325
|
+
|
|
326
|
+
def test_get_credential_logs_masked_value(self, caplog):
|
|
327
|
+
"""Test _get_credential logs masked value."""
|
|
328
|
+
import logging
|
|
329
|
+
|
|
330
|
+
caplog.set_level(logging.INFO)
|
|
331
|
+
task_config = TaskConfig({"options": {"env_path": ".env"}})
|
|
332
|
+
|
|
333
|
+
task = SecretsToEnv(
|
|
334
|
+
project_config=mock.Mock(),
|
|
335
|
+
task_config=task_config,
|
|
336
|
+
org_config=None,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
mock_provider = mock.Mock()
|
|
340
|
+
mock_provider.provider_type = "local"
|
|
341
|
+
mock_provider.get_credentials.return_value = "secret_value"
|
|
342
|
+
task.provider = mock_provider
|
|
343
|
+
|
|
344
|
+
task._get_credential("API_KEY", "api_key")
|
|
345
|
+
|
|
346
|
+
# Check that the log contains masked value
|
|
347
|
+
assert "API_KEY=*****" in caplog.text
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class TestSecretsToEnvGetAllCredentials:
|
|
351
|
+
"""Test cases for _get_all_credentials method."""
|
|
352
|
+
|
|
353
|
+
def test_get_all_credentials_success(self):
|
|
354
|
+
"""Test _get_all_credentials successfully retrieves all credentials."""
|
|
355
|
+
task_config = TaskConfig({"options": {"env_path": ".env"}})
|
|
356
|
+
|
|
357
|
+
task = SecretsToEnv(
|
|
358
|
+
project_config=mock.Mock(),
|
|
359
|
+
task_config=task_config,
|
|
360
|
+
org_config=None,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
mock_provider = mock.Mock()
|
|
364
|
+
mock_provider.provider_type = "aws_secrets"
|
|
365
|
+
mock_provider.get_all_credentials.return_value = {
|
|
366
|
+
"API_KEY": "api_value",
|
|
367
|
+
"DB_PASSWORD": "db_pass",
|
|
368
|
+
"TOKEN": "token_value",
|
|
369
|
+
}
|
|
370
|
+
task.provider = mock_provider
|
|
371
|
+
|
|
372
|
+
result = task._get_all_credentials("*", secret_name="my-app/secrets")
|
|
373
|
+
|
|
374
|
+
assert result == {
|
|
375
|
+
"API_KEY": "api_value",
|
|
376
|
+
"DB_PASSWORD": "db_pass",
|
|
377
|
+
"TOKEN": "token_value",
|
|
378
|
+
}
|
|
379
|
+
mock_provider.get_all_credentials.assert_called_once_with(
|
|
380
|
+
"*", {"secret_name": "my-app/secrets"}
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def test_get_all_credentials_escapes_quotes(self):
|
|
384
|
+
"""Test _get_all_credentials escapes quotes in all values."""
|
|
385
|
+
task_config = TaskConfig({"options": {"env_path": ".env"}})
|
|
386
|
+
|
|
387
|
+
task = SecretsToEnv(
|
|
388
|
+
project_config=mock.Mock(),
|
|
389
|
+
task_config=task_config,
|
|
390
|
+
org_config=None,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
mock_provider = mock.Mock()
|
|
394
|
+
mock_provider.provider_type = "aws_secrets"
|
|
395
|
+
mock_provider.get_all_credentials.return_value = {
|
|
396
|
+
"KEY1": 'value_with_"quotes"',
|
|
397
|
+
"KEY2": "normal_value",
|
|
398
|
+
}
|
|
399
|
+
task.provider = mock_provider
|
|
400
|
+
|
|
401
|
+
result = task._get_all_credentials("*")
|
|
402
|
+
|
|
403
|
+
assert result["KEY1"] == 'value_with_\\"quotes\\"'
|
|
404
|
+
assert result["KEY2"] == "normal_value"
|
|
405
|
+
|
|
406
|
+
def test_get_all_credentials_escapes_newlines(self):
|
|
407
|
+
"""Test _get_all_credentials escapes newlines in all values."""
|
|
408
|
+
task_config = TaskConfig({"options": {"env_path": ".env"}})
|
|
409
|
+
|
|
410
|
+
task = SecretsToEnv(
|
|
411
|
+
project_config=mock.Mock(),
|
|
412
|
+
task_config=task_config,
|
|
413
|
+
org_config=None,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
mock_provider = mock.Mock()
|
|
417
|
+
mock_provider.provider_type = "aws_secrets"
|
|
418
|
+
mock_provider.get_all_credentials.return_value = {
|
|
419
|
+
"KEY1": "line1\nline2",
|
|
420
|
+
"KEY2": "single_line",
|
|
421
|
+
}
|
|
422
|
+
task.provider = mock_provider
|
|
423
|
+
|
|
424
|
+
result = task._get_all_credentials("*")
|
|
425
|
+
|
|
426
|
+
assert result["KEY1"] == "line1\\nline2"
|
|
427
|
+
assert result["KEY2"] == "single_line"
|
|
428
|
+
|
|
429
|
+
def test_get_all_credentials_with_none_value_raises_error(self):
|
|
430
|
+
"""Test _get_all_credentials raises error when provider returns None."""
|
|
431
|
+
task_config = TaskConfig({"options": {"env_path": ".env"}})
|
|
432
|
+
|
|
433
|
+
task = SecretsToEnv(
|
|
434
|
+
project_config=mock.Mock(),
|
|
435
|
+
task_config=task_config,
|
|
436
|
+
org_config=None,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
mock_provider = mock.Mock()
|
|
440
|
+
mock_provider.provider_type = "aws_secrets"
|
|
441
|
+
mock_provider.get_all_credentials.return_value = None
|
|
442
|
+
task.provider = mock_provider
|
|
443
|
+
|
|
444
|
+
with pytest.raises(ValueError) as exc_info:
|
|
445
|
+
task._get_all_credentials("*", secret_name="my-secret")
|
|
446
|
+
|
|
447
|
+
assert "Failed to retrieve secret *(my-secret) from aws_secrets" in str(
|
|
448
|
+
exc_info.value
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def test_get_all_credentials_logs_masked_values(self, caplog):
|
|
452
|
+
"""Test _get_all_credentials logs masked values for all keys."""
|
|
453
|
+
import logging
|
|
454
|
+
|
|
455
|
+
caplog.set_level(logging.INFO)
|
|
456
|
+
task_config = TaskConfig({"options": {"env_path": ".env"}})
|
|
457
|
+
|
|
458
|
+
task = SecretsToEnv(
|
|
459
|
+
project_config=mock.Mock(),
|
|
460
|
+
task_config=task_config,
|
|
461
|
+
org_config=None,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
mock_provider = mock.Mock()
|
|
465
|
+
mock_provider.provider_type = "aws_secrets"
|
|
466
|
+
mock_provider.get_all_credentials.return_value = {
|
|
467
|
+
"API_KEY": "api_value",
|
|
468
|
+
"DB_PASSWORD": "db_pass",
|
|
469
|
+
}
|
|
470
|
+
task.provider = mock_provider
|
|
471
|
+
|
|
472
|
+
task._get_all_credentials("*")
|
|
473
|
+
|
|
474
|
+
# Check that logs contain masked values
|
|
475
|
+
assert "API_KEY=*****" in caplog.text
|
|
476
|
+
assert "DB_PASSWORD=*****" in caplog.text
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class TestSecretsToEnvRunTask:
|
|
480
|
+
"""Test cases for _run_task method."""
|
|
481
|
+
|
|
482
|
+
def test_run_task_with_simple_secrets(self):
|
|
483
|
+
"""Test _run_task with simple list of secrets."""
|
|
484
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
485
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
486
|
+
|
|
487
|
+
task_config = TaskConfig(
|
|
488
|
+
{
|
|
489
|
+
"options": {
|
|
490
|
+
"env_path": env_path,
|
|
491
|
+
"secrets": ["API_KEY", "DB_PASSWORD"],
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
project_config = mock.Mock()
|
|
497
|
+
project_config.repo_root = tmpdir
|
|
498
|
+
|
|
499
|
+
task = SecretsToEnv(
|
|
500
|
+
project_config=project_config,
|
|
501
|
+
task_config=task_config,
|
|
502
|
+
org_config=None,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
mock_provider = mock.Mock()
|
|
506
|
+
mock_provider.provider_type = "local"
|
|
507
|
+
mock_provider.get_credentials.side_effect = [
|
|
508
|
+
"api_secret_123",
|
|
509
|
+
"db_pass_456",
|
|
510
|
+
]
|
|
511
|
+
task.provider = mock_provider
|
|
512
|
+
|
|
513
|
+
task()
|
|
514
|
+
|
|
515
|
+
# Verify file was created
|
|
516
|
+
assert os.path.exists(env_path)
|
|
517
|
+
|
|
518
|
+
# Verify file contents
|
|
519
|
+
with open(env_path, "r") as f:
|
|
520
|
+
content = f.read()
|
|
521
|
+
|
|
522
|
+
assert 'API_KEY="api_secret_123"' in content
|
|
523
|
+
assert 'DB_PASSWORD="db_pass_456"' in content
|
|
524
|
+
|
|
525
|
+
def test_run_task_with_wildcard_secret(self):
|
|
526
|
+
"""Test _run_task with wildcard to get all secrets."""
|
|
527
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
528
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
529
|
+
|
|
530
|
+
task_config = TaskConfig(
|
|
531
|
+
{
|
|
532
|
+
"options": {
|
|
533
|
+
"env_path": env_path,
|
|
534
|
+
"secrets": {"*": "my-app/secrets"},
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
project_config = mock.Mock()
|
|
540
|
+
project_config.repo_root = tmpdir
|
|
541
|
+
|
|
542
|
+
task = SecretsToEnv(
|
|
543
|
+
project_config=project_config,
|
|
544
|
+
task_config=task_config,
|
|
545
|
+
org_config=None,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
# When secrets is a dict, _init_secrets doesn't set task.secrets
|
|
549
|
+
# so we need to set it manually for this test
|
|
550
|
+
task.secrets = task.parsed_options.secrets
|
|
551
|
+
|
|
552
|
+
mock_provider = mock.Mock()
|
|
553
|
+
mock_provider.provider_type = "aws_secrets"
|
|
554
|
+
mock_provider.get_all_credentials.return_value = {
|
|
555
|
+
"API_KEY": "api_value",
|
|
556
|
+
"DB_PASSWORD": "db_pass",
|
|
557
|
+
"TOKEN": "token_value",
|
|
558
|
+
}
|
|
559
|
+
task.provider = mock_provider
|
|
560
|
+
|
|
561
|
+
task()
|
|
562
|
+
|
|
563
|
+
# Verify file was created
|
|
564
|
+
assert os.path.exists(env_path)
|
|
565
|
+
|
|
566
|
+
# Verify file contents
|
|
567
|
+
with open(env_path, "r") as f:
|
|
568
|
+
content = f.read()
|
|
569
|
+
|
|
570
|
+
assert 'API_KEY="api_value"' in content
|
|
571
|
+
assert 'DB_PASSWORD="db_pass"' in content
|
|
572
|
+
assert 'TOKEN="token_value"' in content
|
|
573
|
+
|
|
574
|
+
def test_run_task_creates_directory_if_not_exists(self):
|
|
575
|
+
"""Test _run_task creates parent directory if it doesn't exist."""
|
|
576
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
577
|
+
env_path = os.path.join(tmpdir, "subdir", "nested", ".env")
|
|
578
|
+
|
|
579
|
+
task_config = TaskConfig(
|
|
580
|
+
{
|
|
581
|
+
"options": {
|
|
582
|
+
"env_path": env_path,
|
|
583
|
+
"secrets": ["API_KEY"],
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
project_config = mock.Mock()
|
|
589
|
+
project_config.repo_root = tmpdir
|
|
590
|
+
|
|
591
|
+
task = SecretsToEnv(
|
|
592
|
+
project_config=project_config,
|
|
593
|
+
task_config=task_config,
|
|
594
|
+
org_config=None,
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
mock_provider = mock.Mock()
|
|
598
|
+
mock_provider.provider_type = "local"
|
|
599
|
+
mock_provider.get_credentials.return_value = "api_secret"
|
|
600
|
+
task.provider = mock_provider
|
|
601
|
+
|
|
602
|
+
task()
|
|
603
|
+
|
|
604
|
+
# Verify directory and file were created
|
|
605
|
+
assert os.path.exists(os.path.dirname(env_path))
|
|
606
|
+
assert os.path.exists(env_path)
|
|
607
|
+
|
|
608
|
+
def test_run_task_preserves_existing_env_values(self):
|
|
609
|
+
"""Test _run_task preserves existing environment variables."""
|
|
610
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
611
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
612
|
+
|
|
613
|
+
# Create existing .env file
|
|
614
|
+
with open(env_path, "w") as f:
|
|
615
|
+
f.write('EXISTING_KEY="existing_value"\n')
|
|
616
|
+
f.write('ANOTHER_KEY="another_value"\n')
|
|
617
|
+
|
|
618
|
+
task_config = TaskConfig(
|
|
619
|
+
{
|
|
620
|
+
"options": {
|
|
621
|
+
"env_path": env_path,
|
|
622
|
+
"secrets": ["NEW_SECRET"],
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
project_config = mock.Mock()
|
|
628
|
+
project_config.repo_root = tmpdir
|
|
629
|
+
|
|
630
|
+
task = SecretsToEnv(
|
|
631
|
+
project_config=project_config,
|
|
632
|
+
task_config=task_config,
|
|
633
|
+
org_config=None,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
mock_provider = mock.Mock()
|
|
637
|
+
mock_provider.provider_type = "local"
|
|
638
|
+
mock_provider.get_credentials.return_value = "new_secret_value"
|
|
639
|
+
task.provider = mock_provider
|
|
640
|
+
|
|
641
|
+
task()
|
|
642
|
+
|
|
643
|
+
# Verify file contents
|
|
644
|
+
with open(env_path, "r") as f:
|
|
645
|
+
content = f.read()
|
|
646
|
+
|
|
647
|
+
assert 'EXISTING_KEY="existing_value"' in content
|
|
648
|
+
assert 'ANOTHER_KEY="another_value"' in content
|
|
649
|
+
assert 'NEW_SECRET="new_secret_value"' in content
|
|
650
|
+
|
|
651
|
+
def test_run_task_overwrites_duplicate_keys(self):
|
|
652
|
+
"""Test _run_task overwrites duplicate keys with new values."""
|
|
653
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
654
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
655
|
+
|
|
656
|
+
# Create existing .env file with key to be overwritten
|
|
657
|
+
with open(env_path, "w") as f:
|
|
658
|
+
f.write('API_KEY="old_value"\n')
|
|
659
|
+
|
|
660
|
+
task_config = TaskConfig(
|
|
661
|
+
{
|
|
662
|
+
"options": {
|
|
663
|
+
"env_path": env_path,
|
|
664
|
+
"secrets": ["API_KEY"],
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
project_config = mock.Mock()
|
|
670
|
+
project_config.repo_root = tmpdir
|
|
671
|
+
|
|
672
|
+
task = SecretsToEnv(
|
|
673
|
+
project_config=project_config,
|
|
674
|
+
task_config=task_config,
|
|
675
|
+
org_config=None,
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
mock_provider = mock.Mock()
|
|
679
|
+
mock_provider.provider_type = "local"
|
|
680
|
+
mock_provider.get_credentials.return_value = "new_value"
|
|
681
|
+
task.provider = mock_provider
|
|
682
|
+
|
|
683
|
+
task()
|
|
684
|
+
|
|
685
|
+
# Verify file contents
|
|
686
|
+
with open(env_path, "r") as f:
|
|
687
|
+
content = f.read()
|
|
688
|
+
|
|
689
|
+
assert 'API_KEY="new_value"' in content
|
|
690
|
+
assert 'API_KEY="old_value"' not in content
|
|
691
|
+
|
|
692
|
+
def test_run_task_with_mixed_secrets_and_wildcard(self):
|
|
693
|
+
"""Test _run_task with combination of specific secrets and wildcard."""
|
|
694
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
695
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
696
|
+
|
|
697
|
+
task_config = TaskConfig(
|
|
698
|
+
{
|
|
699
|
+
"options": {
|
|
700
|
+
"env_path": env_path,
|
|
701
|
+
"secrets": {
|
|
702
|
+
"*": "my-app/secrets",
|
|
703
|
+
"SPECIFIC_KEY": "specific_value",
|
|
704
|
+
},
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
project_config = mock.Mock()
|
|
710
|
+
project_config.repo_root = tmpdir
|
|
711
|
+
|
|
712
|
+
task = SecretsToEnv(
|
|
713
|
+
project_config=project_config,
|
|
714
|
+
task_config=task_config,
|
|
715
|
+
org_config=None,
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
# When secrets is a dict, _init_secrets doesn't set task.secrets
|
|
719
|
+
# so we need to set it manually for this test
|
|
720
|
+
task.secrets = task.parsed_options.secrets
|
|
721
|
+
|
|
722
|
+
mock_provider = mock.Mock()
|
|
723
|
+
mock_provider.provider_type = "aws_secrets"
|
|
724
|
+
|
|
725
|
+
def get_credentials_side_effect(key, options):
|
|
726
|
+
if key == "SPECIFIC_KEY":
|
|
727
|
+
return "specific_secret"
|
|
728
|
+
return None
|
|
729
|
+
|
|
730
|
+
mock_provider.get_credentials.side_effect = get_credentials_side_effect
|
|
731
|
+
mock_provider.get_all_credentials.return_value = {
|
|
732
|
+
"WILDCARD_KEY1": "wildcard_value1",
|
|
733
|
+
"WILDCARD_KEY2": "wildcard_value2",
|
|
734
|
+
}
|
|
735
|
+
task.provider = mock_provider
|
|
736
|
+
|
|
737
|
+
task()
|
|
738
|
+
|
|
739
|
+
# Verify file contents
|
|
740
|
+
with open(env_path, "r") as f:
|
|
741
|
+
content = f.read()
|
|
742
|
+
|
|
743
|
+
assert 'WILDCARD_KEY1="wildcard_value1"' in content
|
|
744
|
+
assert 'WILDCARD_KEY2="wildcard_value2"' in content
|
|
745
|
+
assert 'SPECIFIC_KEY="specific_secret"' in content
|
|
746
|
+
|
|
747
|
+
def test_run_task_creates_env_in_current_directory(self):
|
|
748
|
+
"""Test _run_task creates .env in current directory when dirname is empty."""
|
|
749
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
750
|
+
env_path = ".env" # No directory component
|
|
751
|
+
|
|
752
|
+
task_config = TaskConfig(
|
|
753
|
+
{
|
|
754
|
+
"options": {
|
|
755
|
+
"env_path": env_path,
|
|
756
|
+
"secrets": ["API_KEY"],
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
project_config = mock.Mock()
|
|
762
|
+
project_config.repo_root = tmpdir
|
|
763
|
+
|
|
764
|
+
task = SecretsToEnv(
|
|
765
|
+
project_config=project_config,
|
|
766
|
+
task_config=task_config,
|
|
767
|
+
org_config=None,
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
mock_provider = mock.Mock()
|
|
771
|
+
mock_provider.provider_type = "local"
|
|
772
|
+
mock_provider.get_credentials.return_value = "api_secret"
|
|
773
|
+
task.provider = mock_provider
|
|
774
|
+
|
|
775
|
+
task()
|
|
776
|
+
|
|
777
|
+
# Verify file was created in current directory
|
|
778
|
+
full_path = os.path.join(tmpdir, env_path)
|
|
779
|
+
assert os.path.exists(full_path)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
class TestSecretsToEnvIntegration:
|
|
783
|
+
"""Integration tests for SecretsToEnv with different providers."""
|
|
784
|
+
|
|
785
|
+
@mock.patch.dict(os.environ, {"TEST_API_KEY": "env_api_secret"})
|
|
786
|
+
def test_integration_with_environment_provider(self):
|
|
787
|
+
"""Test full workflow with environment provider."""
|
|
788
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
789
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
790
|
+
|
|
791
|
+
task_config = TaskConfig(
|
|
792
|
+
{
|
|
793
|
+
"options": {
|
|
794
|
+
"env_path": env_path,
|
|
795
|
+
"secrets_provider": "environment",
|
|
796
|
+
"provider_options": {"key_prefix": "TEST_"},
|
|
797
|
+
"secrets": ["API_KEY"],
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
project_config = mock.Mock()
|
|
803
|
+
project_config.repo_root = tmpdir
|
|
804
|
+
|
|
805
|
+
task = SecretsToEnv(
|
|
806
|
+
project_config=project_config,
|
|
807
|
+
task_config=task_config,
|
|
808
|
+
org_config=None,
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
task()
|
|
812
|
+
|
|
813
|
+
# Verify file contents
|
|
814
|
+
with open(env_path, "r") as f:
|
|
815
|
+
content = f.read()
|
|
816
|
+
|
|
817
|
+
assert 'API_KEY="env_api_secret"' in content
|
|
818
|
+
|
|
819
|
+
def test_integration_with_local_provider(self):
|
|
820
|
+
"""Test full workflow with local provider."""
|
|
821
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
822
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
823
|
+
|
|
824
|
+
task_config = TaskConfig(
|
|
825
|
+
{
|
|
826
|
+
"options": {
|
|
827
|
+
"env_path": env_path,
|
|
828
|
+
"secrets_provider": "local",
|
|
829
|
+
"secrets": ["API_KEY", "DB_PASS"],
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
project_config = mock.Mock()
|
|
835
|
+
project_config.repo_root = tmpdir
|
|
836
|
+
|
|
837
|
+
task = SecretsToEnv(
|
|
838
|
+
project_config=project_config,
|
|
839
|
+
task_config=task_config,
|
|
840
|
+
org_config=None,
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
task()
|
|
844
|
+
|
|
845
|
+
# Verify file contents
|
|
846
|
+
# Local provider returns the key itself as the value
|
|
847
|
+
with open(env_path, "r") as f:
|
|
848
|
+
content = f.read()
|
|
849
|
+
|
|
850
|
+
assert 'API_KEY="API_KEY"' in content
|
|
851
|
+
assert 'DB_PASS="DB_PASS"' in content
|
|
852
|
+
|
|
853
|
+
def test_integration_with_aws_provider(self):
|
|
854
|
+
"""Test full workflow with AWS Secrets Manager provider."""
|
|
855
|
+
import json
|
|
856
|
+
import sys
|
|
857
|
+
|
|
858
|
+
mock_client = mock.Mock()
|
|
859
|
+
mock_session = mock.Mock()
|
|
860
|
+
mock_session.client.return_value = mock_client
|
|
861
|
+
mock_boto3 = mock.Mock()
|
|
862
|
+
mock_boto3.session.Session.return_value = mock_session
|
|
863
|
+
|
|
864
|
+
secret_data = {"API_KEY": "aws_api_value", "DB_PASSWORD": "aws_db_pass"}
|
|
865
|
+
mock_client.get_secret_value.return_value = {
|
|
866
|
+
"SecretString": json.dumps(secret_data)
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
870
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
871
|
+
|
|
872
|
+
task_config = TaskConfig(
|
|
873
|
+
{
|
|
874
|
+
"options": {
|
|
875
|
+
"env_path": env_path,
|
|
876
|
+
"secrets_provider": "aws_secrets",
|
|
877
|
+
"provider_options": {"aws_region": "us-east-1"},
|
|
878
|
+
"secrets": {"*": "my-app/secrets"},
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
project_config = mock.Mock()
|
|
884
|
+
project_config.repo_root = tmpdir
|
|
885
|
+
|
|
886
|
+
with mock.patch.dict(
|
|
887
|
+
sys.modules, {"boto3": mock_boto3, "botocore.exceptions": mock.Mock()}
|
|
888
|
+
):
|
|
889
|
+
task = SecretsToEnv(
|
|
890
|
+
project_config=project_config,
|
|
891
|
+
task_config=task_config,
|
|
892
|
+
org_config=None,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
# When secrets is a dict, _init_secrets doesn't set task.secrets
|
|
896
|
+
# so we need to set it manually for this test
|
|
897
|
+
task.secrets = task.parsed_options.secrets
|
|
898
|
+
|
|
899
|
+
task()
|
|
900
|
+
|
|
901
|
+
# Verify file contents
|
|
902
|
+
with open(env_path, "r") as f:
|
|
903
|
+
content = f.read()
|
|
904
|
+
|
|
905
|
+
assert 'API_KEY="aws_api_value"' in content
|
|
906
|
+
assert 'DB_PASSWORD="aws_db_pass"' in content
|
|
907
|
+
|
|
908
|
+
@mock.patch.dict(os.environ, {"MYAPP_API_TOKEN": "ado_token_value"})
|
|
909
|
+
def test_integration_with_ado_provider(self):
|
|
910
|
+
"""Test full workflow with Azure DevOps variables provider."""
|
|
911
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
912
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
913
|
+
|
|
914
|
+
task_config = TaskConfig(
|
|
915
|
+
{
|
|
916
|
+
"options": {
|
|
917
|
+
"env_path": env_path,
|
|
918
|
+
"secrets_provider": "ado_variables",
|
|
919
|
+
"provider_options": {"key_prefix": "MYAPP_"},
|
|
920
|
+
"secrets": ["API_TOKEN"],
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
project_config = mock.Mock()
|
|
926
|
+
project_config.repo_root = tmpdir
|
|
927
|
+
|
|
928
|
+
task = SecretsToEnv(
|
|
929
|
+
project_config=project_config,
|
|
930
|
+
task_config=task_config,
|
|
931
|
+
org_config=None,
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
task()
|
|
935
|
+
|
|
936
|
+
# Verify file contents
|
|
937
|
+
with open(env_path, "r") as f:
|
|
938
|
+
content = f.read()
|
|
939
|
+
|
|
940
|
+
assert 'API_TOKEN="ado_token_value"' in content
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
class TestSecretsToEnvEdgeCases:
|
|
944
|
+
"""Test edge cases and error conditions."""
|
|
945
|
+
|
|
946
|
+
def test_empty_secrets_list_creates_empty_env_file(self):
|
|
947
|
+
"""Test that empty secrets list still creates/updates env file."""
|
|
948
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
949
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
950
|
+
|
|
951
|
+
# Create existing .env file
|
|
952
|
+
with open(env_path, "w") as f:
|
|
953
|
+
f.write('EXISTING="value"\n')
|
|
954
|
+
|
|
955
|
+
task_config = TaskConfig(
|
|
956
|
+
{
|
|
957
|
+
"options": {
|
|
958
|
+
"env_path": env_path,
|
|
959
|
+
"secrets": [],
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
project_config = mock.Mock()
|
|
965
|
+
project_config.repo_root = tmpdir
|
|
966
|
+
|
|
967
|
+
task = SecretsToEnv(
|
|
968
|
+
project_config=project_config,
|
|
969
|
+
task_config=task_config,
|
|
970
|
+
org_config=None,
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
# When secrets is an empty list, _init_secrets doesn't set task.secrets
|
|
974
|
+
# so we need to set it manually for this test
|
|
975
|
+
task.secrets = {}
|
|
976
|
+
|
|
977
|
+
task()
|
|
978
|
+
|
|
979
|
+
# Verify existing content is preserved
|
|
980
|
+
with open(env_path, "r") as f:
|
|
981
|
+
content = f.read()
|
|
982
|
+
|
|
983
|
+
assert 'EXISTING="value"' in content
|
|
984
|
+
|
|
985
|
+
def test_special_characters_in_secret_values(self):
|
|
986
|
+
"""Test handling of special characters in secret values."""
|
|
987
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
988
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
989
|
+
|
|
990
|
+
task_config = TaskConfig(
|
|
991
|
+
{
|
|
992
|
+
"options": {
|
|
993
|
+
"env_path": env_path,
|
|
994
|
+
"secrets": ["SPECIAL"],
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
project_config = mock.Mock()
|
|
1000
|
+
project_config.repo_root = tmpdir
|
|
1001
|
+
|
|
1002
|
+
task = SecretsToEnv(
|
|
1003
|
+
project_config=project_config,
|
|
1004
|
+
task_config=task_config,
|
|
1005
|
+
org_config=None,
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
mock_provider = mock.Mock()
|
|
1009
|
+
mock_provider.provider_type = "local"
|
|
1010
|
+
mock_provider.get_credentials.return_value = (
|
|
1011
|
+
"value!@#$%^&*()[]{}|\\;':<>?,./~`"
|
|
1012
|
+
)
|
|
1013
|
+
task.provider = mock_provider
|
|
1014
|
+
|
|
1015
|
+
task()
|
|
1016
|
+
|
|
1017
|
+
# Verify file can be read
|
|
1018
|
+
assert os.path.exists(env_path)
|
|
1019
|
+
|
|
1020
|
+
def test_unicode_characters_in_secret_values(self):
|
|
1021
|
+
"""Test handling of unicode characters in secret values."""
|
|
1022
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1023
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1024
|
+
|
|
1025
|
+
task_config = TaskConfig(
|
|
1026
|
+
{
|
|
1027
|
+
"options": {
|
|
1028
|
+
"env_path": env_path,
|
|
1029
|
+
"secrets": ["UNICODE"],
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
project_config = mock.Mock()
|
|
1035
|
+
project_config.repo_root = tmpdir
|
|
1036
|
+
|
|
1037
|
+
task = SecretsToEnv(
|
|
1038
|
+
project_config=project_config,
|
|
1039
|
+
task_config=task_config,
|
|
1040
|
+
org_config=None,
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
mock_provider = mock.Mock()
|
|
1044
|
+
mock_provider.provider_type = "local"
|
|
1045
|
+
mock_provider.get_credentials.return_value = "Hello 世界 🌍 Привет"
|
|
1046
|
+
task.provider = mock_provider
|
|
1047
|
+
|
|
1048
|
+
task()
|
|
1049
|
+
|
|
1050
|
+
# Verify file contents
|
|
1051
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
|
1052
|
+
content = f.read()
|
|
1053
|
+
|
|
1054
|
+
assert 'UNICODE="Hello 世界 🌍 Привет"' in content
|
|
1055
|
+
|
|
1056
|
+
def test_very_long_secret_value(self):
|
|
1057
|
+
"""Test handling of very long secret values."""
|
|
1058
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1059
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1060
|
+
|
|
1061
|
+
task_config = TaskConfig(
|
|
1062
|
+
{
|
|
1063
|
+
"options": {
|
|
1064
|
+
"env_path": env_path,
|
|
1065
|
+
"secrets": ["LONG_SECRET"],
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
project_config = mock.Mock()
|
|
1071
|
+
project_config.repo_root = tmpdir
|
|
1072
|
+
|
|
1073
|
+
task = SecretsToEnv(
|
|
1074
|
+
project_config=project_config,
|
|
1075
|
+
task_config=task_config,
|
|
1076
|
+
org_config=None,
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
mock_provider = mock.Mock()
|
|
1080
|
+
mock_provider.provider_type = "local"
|
|
1081
|
+
long_value = "x" * 10000 # 10k characters
|
|
1082
|
+
mock_provider.get_credentials.return_value = long_value
|
|
1083
|
+
task.provider = mock_provider
|
|
1084
|
+
|
|
1085
|
+
task()
|
|
1086
|
+
|
|
1087
|
+
# Verify file contents
|
|
1088
|
+
with open(env_path, "r") as f:
|
|
1089
|
+
content = f.read()
|
|
1090
|
+
|
|
1091
|
+
assert f'LONG_SECRET="{long_value}"' in content
|