cumulusci-plus 5.0.35__py3-none-any.whl → 5.0.45__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cumulusci/__about__.py +1 -1
- cumulusci/cli/cci.py +3 -2
- cumulusci/cli/task.py +9 -10
- cumulusci/cli/tests/test_org.py +5 -0
- cumulusci/cli/tests/test_task.py +34 -0
- cumulusci/core/config/__init__.py +1 -0
- cumulusci/core/config/org_config.py +2 -1
- cumulusci/core/config/project_config.py +12 -0
- cumulusci/core/config/scratch_org_config.py +12 -0
- cumulusci/core/config/sfdx_org_config.py +4 -1
- cumulusci/core/config/tests/test_config.py +1 -0
- cumulusci/core/dependencies/base.py +4 -0
- cumulusci/cumulusci.yml +18 -1
- cumulusci/schema/cumulusci.jsonschema.json +5 -0
- cumulusci/tasks/apex/testrunner.py +7 -4
- cumulusci/tasks/bulkdata/tests/test_select_utils.py +20 -0
- cumulusci/tasks/metadata_etl/__init__.py +2 -0
- cumulusci/tasks/metadata_etl/applications.py +256 -0
- cumulusci/tasks/metadata_etl/tests/test_applications.py +710 -0
- cumulusci/tasks/salesforce/insert_record.py +18 -19
- cumulusci/tasks/salesforce/tests/test_enable_prediction.py +4 -2
- cumulusci/tasks/salesforce/tests/test_update_external_auth_identity_provider.py +927 -0
- cumulusci/tasks/salesforce/tests/test_update_external_credential.py +523 -8
- cumulusci/tasks/salesforce/tests/test_update_record.py +512 -0
- cumulusci/tasks/salesforce/update_external_auth_identity_provider.py +551 -0
- cumulusci/tasks/salesforce/update_external_credential.py +89 -4
- cumulusci/tasks/salesforce/update_record.py +217 -0
- cumulusci/tasks/sfdmu/sfdmu.py +14 -1
- cumulusci/tasks/utility/credentialManager.py +58 -12
- cumulusci/tasks/utility/secretsToEnv.py +42 -11
- cumulusci/tasks/utility/tests/test_credentialManager.py +586 -0
- cumulusci/tasks/utility/tests/test_secretsToEnv.py +1240 -62
- cumulusci/utils/yaml/cumulusci_yml.py +1 -0
- {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/METADATA +5 -7
- {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/RECORD +39 -33
- {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/WHEEL +1 -1
- {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.35.dist-info → cumulusci_plus-5.0.45.dist-info}/licenses/LICENSE +0 -0
|
@@ -17,7 +17,7 @@ class TestSecretsToEnvOptions:
|
|
|
17
17
|
|
|
18
18
|
def test_default_options(self):
|
|
19
19
|
"""Test initialization with default options."""
|
|
20
|
-
task_config = TaskConfig({"options": {}})
|
|
20
|
+
task_config = TaskConfig({"options": {"secrets": ["TEST_SECRET"]}})
|
|
21
21
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
22
22
|
env_path = os.path.join(tmpdir, ".env")
|
|
23
23
|
task_config.options["env_path"] = env_path
|
|
@@ -31,7 +31,7 @@ class TestSecretsToEnvOptions:
|
|
|
31
31
|
assert task.parsed_options.env_path == Path(env_path)
|
|
32
32
|
assert task.parsed_options.secrets_provider is None
|
|
33
33
|
assert task.parsed_options.provider_options == {}
|
|
34
|
-
assert task.parsed_options.secrets == []
|
|
34
|
+
assert task.parsed_options.secrets == ["TEST_SECRET"]
|
|
35
35
|
|
|
36
36
|
def test_custom_options(self):
|
|
37
37
|
"""Test initialization with custom options."""
|
|
@@ -90,6 +90,7 @@ class TestSecretsToEnvInitialization:
|
|
|
90
90
|
"options": {
|
|
91
91
|
"env_path": ".env",
|
|
92
92
|
"secrets_provider": "local",
|
|
93
|
+
"secrets": ["TEST_SECRET"],
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
)
|
|
@@ -112,7 +113,9 @@ class TestSecretsToEnvInitialization:
|
|
|
112
113
|
with open(env_path, "w") as f:
|
|
113
114
|
f.write('EXISTING_KEY="existing_value"\n')
|
|
114
115
|
|
|
115
|
-
task_config = TaskConfig(
|
|
116
|
+
task_config = TaskConfig(
|
|
117
|
+
{"options": {"env_path": env_path, "secrets": ["TEST_SECRET"]}}
|
|
118
|
+
)
|
|
116
119
|
|
|
117
120
|
task = SecretsToEnv(
|
|
118
121
|
project_config=mock.Mock(),
|
|
@@ -202,7 +205,9 @@ class TestSecretsToEnvGetCredential:
|
|
|
202
205
|
|
|
203
206
|
def test_get_credential_success(self):
|
|
204
207
|
"""Test _get_credential successfully retrieves credential."""
|
|
205
|
-
task_config = TaskConfig(
|
|
208
|
+
task_config = TaskConfig(
|
|
209
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
210
|
+
)
|
|
206
211
|
|
|
207
212
|
task = SecretsToEnv(
|
|
208
213
|
project_config=mock.Mock(),
|
|
@@ -215,11 +220,10 @@ class TestSecretsToEnvGetCredential:
|
|
|
215
220
|
mock_provider.get_credentials.return_value = "secret_value_123"
|
|
216
221
|
task.provider = mock_provider
|
|
217
222
|
|
|
218
|
-
|
|
223
|
+
original_value = task._get_credential(
|
|
219
224
|
"API_KEY", "api_key", secret_name="my-secret"
|
|
220
225
|
)
|
|
221
226
|
|
|
222
|
-
assert safe_value == "secret_value_123"
|
|
223
227
|
assert original_value == "secret_value_123"
|
|
224
228
|
mock_provider.get_credentials.assert_called_once_with(
|
|
225
229
|
"API_KEY", {"value": "api_key", "secret_name": "my-secret"}
|
|
@@ -227,7 +231,9 @@ class TestSecretsToEnvGetCredential:
|
|
|
227
231
|
|
|
228
232
|
def test_get_credential_escapes_quotes(self):
|
|
229
233
|
"""Test _get_credential escapes double quotes."""
|
|
230
|
-
task_config = TaskConfig(
|
|
234
|
+
task_config = TaskConfig(
|
|
235
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
236
|
+
)
|
|
231
237
|
|
|
232
238
|
task = SecretsToEnv(
|
|
233
239
|
project_config=mock.Mock(),
|
|
@@ -240,13 +246,15 @@ class TestSecretsToEnvGetCredential:
|
|
|
240
246
|
mock_provider.get_credentials.return_value = 'value_with_"quotes"'
|
|
241
247
|
task.provider = mock_provider
|
|
242
248
|
|
|
243
|
-
|
|
249
|
+
original_value = task._get_credential("API_KEY", "api_key")
|
|
244
250
|
|
|
245
|
-
assert
|
|
251
|
+
assert original_value == 'value_with_"quotes"'
|
|
246
252
|
|
|
247
253
|
def test_get_credential_escapes_newlines(self):
|
|
248
254
|
"""Test _get_credential escapes newlines."""
|
|
249
|
-
task_config = TaskConfig(
|
|
255
|
+
task_config = TaskConfig(
|
|
256
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
257
|
+
)
|
|
250
258
|
|
|
251
259
|
task = SecretsToEnv(
|
|
252
260
|
project_config=mock.Mock(),
|
|
@@ -259,13 +267,15 @@ class TestSecretsToEnvGetCredential:
|
|
|
259
267
|
mock_provider.get_credentials.return_value = "line1\nline2\nline3"
|
|
260
268
|
task.provider = mock_provider
|
|
261
269
|
|
|
262
|
-
|
|
270
|
+
original_value = task._get_credential("API_KEY", "api_key")
|
|
263
271
|
|
|
264
|
-
assert
|
|
272
|
+
assert original_value == "line1\nline2\nline3"
|
|
265
273
|
|
|
266
274
|
def test_get_credential_handles_both_quotes_and_newlines(self):
|
|
267
275
|
"""Test _get_credential handles both quotes and newlines."""
|
|
268
|
-
task_config = TaskConfig(
|
|
276
|
+
task_config = TaskConfig(
|
|
277
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
278
|
+
)
|
|
269
279
|
|
|
270
280
|
task = SecretsToEnv(
|
|
271
281
|
project_config=mock.Mock(),
|
|
@@ -278,13 +288,15 @@ class TestSecretsToEnvGetCredential:
|
|
|
278
288
|
mock_provider.get_credentials.return_value = 'line1 "quoted"\nline2'
|
|
279
289
|
task.provider = mock_provider
|
|
280
290
|
|
|
281
|
-
|
|
291
|
+
original_value = task._get_credential("API_KEY", "api_key")
|
|
282
292
|
|
|
283
|
-
assert
|
|
293
|
+
assert original_value == 'line1 "quoted"\nline2'
|
|
284
294
|
|
|
285
295
|
def test_get_credential_with_none_value_raises_error(self):
|
|
286
296
|
"""Test _get_credential raises error when provider returns None."""
|
|
287
|
-
task_config = TaskConfig(
|
|
297
|
+
task_config = TaskConfig(
|
|
298
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
299
|
+
)
|
|
288
300
|
|
|
289
301
|
task = SecretsToEnv(
|
|
290
302
|
project_config=mock.Mock(),
|
|
@@ -304,7 +316,9 @@ class TestSecretsToEnvGetCredential:
|
|
|
304
316
|
|
|
305
317
|
def test_get_credential_uses_env_key_parameter(self):
|
|
306
318
|
"""Test _get_credential uses custom env_key when provided."""
|
|
307
|
-
task_config = TaskConfig(
|
|
319
|
+
task_config = TaskConfig(
|
|
320
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
321
|
+
)
|
|
308
322
|
|
|
309
323
|
task = SecretsToEnv(
|
|
310
324
|
project_config=mock.Mock(),
|
|
@@ -317,18 +331,20 @@ class TestSecretsToEnvGetCredential:
|
|
|
317
331
|
mock_provider.get_credentials.return_value = "secret_value"
|
|
318
332
|
task.provider = mock_provider
|
|
319
333
|
|
|
320
|
-
|
|
334
|
+
original_value = task._get_credential(
|
|
321
335
|
"CREDENTIAL_KEY", "value", env_key="CUSTOM_ENV_KEY"
|
|
322
336
|
)
|
|
323
337
|
|
|
324
|
-
assert
|
|
338
|
+
assert original_value == "secret_value"
|
|
325
339
|
|
|
326
340
|
def test_get_credential_logs_masked_value(self, caplog):
|
|
327
341
|
"""Test _get_credential logs masked value."""
|
|
328
342
|
import logging
|
|
329
343
|
|
|
330
344
|
caplog.set_level(logging.INFO)
|
|
331
|
-
task_config = TaskConfig(
|
|
345
|
+
task_config = TaskConfig(
|
|
346
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
347
|
+
)
|
|
332
348
|
|
|
333
349
|
task = SecretsToEnv(
|
|
334
350
|
project_config=mock.Mock(),
|
|
@@ -352,7 +368,9 @@ class TestSecretsToEnvGetAllCredentials:
|
|
|
352
368
|
|
|
353
369
|
def test_get_all_credentials_success(self):
|
|
354
370
|
"""Test _get_all_credentials successfully retrieves all credentials."""
|
|
355
|
-
task_config = TaskConfig(
|
|
371
|
+
task_config = TaskConfig(
|
|
372
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
373
|
+
)
|
|
356
374
|
|
|
357
375
|
task = SecretsToEnv(
|
|
358
376
|
project_config=mock.Mock(),
|
|
@@ -382,7 +400,9 @@ class TestSecretsToEnvGetAllCredentials:
|
|
|
382
400
|
|
|
383
401
|
def test_get_all_credentials_escapes_quotes(self):
|
|
384
402
|
"""Test _get_all_credentials escapes quotes in all values."""
|
|
385
|
-
task_config = TaskConfig(
|
|
403
|
+
task_config = TaskConfig(
|
|
404
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
405
|
+
)
|
|
386
406
|
|
|
387
407
|
task = SecretsToEnv(
|
|
388
408
|
project_config=mock.Mock(),
|
|
@@ -400,12 +420,14 @@ class TestSecretsToEnvGetAllCredentials:
|
|
|
400
420
|
|
|
401
421
|
result = task._get_all_credentials("*")
|
|
402
422
|
|
|
403
|
-
assert result["KEY1"] == 'value_with_
|
|
423
|
+
assert result["KEY1"] == 'value_with_"quotes"'
|
|
404
424
|
assert result["KEY2"] == "normal_value"
|
|
405
425
|
|
|
406
426
|
def test_get_all_credentials_escapes_newlines(self):
|
|
407
427
|
"""Test _get_all_credentials escapes newlines in all values."""
|
|
408
|
-
task_config = TaskConfig(
|
|
428
|
+
task_config = TaskConfig(
|
|
429
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
430
|
+
)
|
|
409
431
|
|
|
410
432
|
task = SecretsToEnv(
|
|
411
433
|
project_config=mock.Mock(),
|
|
@@ -423,12 +445,14 @@ class TestSecretsToEnvGetAllCredentials:
|
|
|
423
445
|
|
|
424
446
|
result = task._get_all_credentials("*")
|
|
425
447
|
|
|
426
|
-
assert result["KEY1"] == "line1
|
|
448
|
+
assert result["KEY1"] == "line1\nline2"
|
|
427
449
|
assert result["KEY2"] == "single_line"
|
|
428
450
|
|
|
429
451
|
def test_get_all_credentials_with_none_value_raises_error(self):
|
|
430
452
|
"""Test _get_all_credentials raises error when provider returns None."""
|
|
431
|
-
task_config = TaskConfig(
|
|
453
|
+
task_config = TaskConfig(
|
|
454
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
455
|
+
)
|
|
432
456
|
|
|
433
457
|
task = SecretsToEnv(
|
|
434
458
|
project_config=mock.Mock(),
|
|
@@ -453,7 +477,9 @@ class TestSecretsToEnvGetAllCredentials:
|
|
|
453
477
|
import logging
|
|
454
478
|
|
|
455
479
|
caplog.set_level(logging.INFO)
|
|
456
|
-
task_config = TaskConfig(
|
|
480
|
+
task_config = TaskConfig(
|
|
481
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
482
|
+
)
|
|
457
483
|
|
|
458
484
|
task = SecretsToEnv(
|
|
459
485
|
project_config=mock.Mock(),
|
|
@@ -940,23 +966,187 @@ class TestSecretsToEnvIntegration:
|
|
|
940
966
|
assert 'API_TOKEN="ado_token_value"' in content
|
|
941
967
|
|
|
942
968
|
|
|
943
|
-
class
|
|
944
|
-
"""Test
|
|
969
|
+
class TestSecretsToEnvReturnValues:
|
|
970
|
+
"""Test cases for return_values from task execution."""
|
|
945
971
|
|
|
946
|
-
def
|
|
947
|
-
"""Test that
|
|
972
|
+
def test_return_values_contains_env_values(self):
|
|
973
|
+
"""Test that return_values contains env_values."""
|
|
974
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
975
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
976
|
+
|
|
977
|
+
task_config = TaskConfig(
|
|
978
|
+
{
|
|
979
|
+
"options": {
|
|
980
|
+
"env_path": env_path,
|
|
981
|
+
"secrets": ["API_KEY"],
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
project_config = mock.Mock()
|
|
987
|
+
project_config.repo_root = tmpdir
|
|
988
|
+
|
|
989
|
+
task = SecretsToEnv(
|
|
990
|
+
project_config=project_config,
|
|
991
|
+
task_config=task_config,
|
|
992
|
+
org_config=None,
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
mock_provider = mock.Mock()
|
|
996
|
+
mock_provider.provider_type = "local"
|
|
997
|
+
mock_provider.get_credentials.return_value = "secret_value"
|
|
998
|
+
task.provider = mock_provider
|
|
999
|
+
|
|
1000
|
+
task()
|
|
1001
|
+
|
|
1002
|
+
# Verify return_values contains env_values
|
|
1003
|
+
assert "env_values" in task.return_values
|
|
1004
|
+
assert "API_KEY" in task.return_values["env_values"]
|
|
1005
|
+
assert task.return_values["env_values"]["API_KEY"] == "secret_value"
|
|
1006
|
+
|
|
1007
|
+
def test_return_values_contains_safe_env_values(self):
|
|
1008
|
+
"""Test that return_values contains safe_env_values with escaped values."""
|
|
1009
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1010
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1011
|
+
|
|
1012
|
+
task_config = TaskConfig(
|
|
1013
|
+
{
|
|
1014
|
+
"options": {
|
|
1015
|
+
"env_path": env_path,
|
|
1016
|
+
"secrets": ["QUOTE_SECRET"],
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
project_config = mock.Mock()
|
|
1022
|
+
project_config.repo_root = tmpdir
|
|
1023
|
+
|
|
1024
|
+
task = SecretsToEnv(
|
|
1025
|
+
project_config=project_config,
|
|
1026
|
+
task_config=task_config,
|
|
1027
|
+
org_config=None,
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
mock_provider = mock.Mock()
|
|
1031
|
+
mock_provider.provider_type = "local"
|
|
1032
|
+
mock_provider.get_credentials.return_value = 'value with "quotes"'
|
|
1033
|
+
task.provider = mock_provider
|
|
1034
|
+
|
|
1035
|
+
task()
|
|
1036
|
+
|
|
1037
|
+
# Verify return_values contains safe_env_values
|
|
1038
|
+
assert "safe_env_values" in task.return_values
|
|
1039
|
+
assert "QUOTE_SECRET" in task.return_values["safe_env_values"]
|
|
1040
|
+
# Verify quotes are escaped in safe_env_values
|
|
1041
|
+
assert (
|
|
1042
|
+
task.return_values["safe_env_values"]["QUOTE_SECRET"]
|
|
1043
|
+
== 'value with \\"quotes\\"'
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
def test_return_values_safe_escapes_all_special_chars(self):
|
|
1047
|
+
"""Test that safe_env_values properly escapes all special characters."""
|
|
1048
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1049
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1050
|
+
|
|
1051
|
+
task_config = TaskConfig(
|
|
1052
|
+
{
|
|
1053
|
+
"options": {
|
|
1054
|
+
"env_path": env_path,
|
|
1055
|
+
"secrets": ["COMPLEX_SECRET"],
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
project_config = mock.Mock()
|
|
1061
|
+
project_config.repo_root = tmpdir
|
|
1062
|
+
|
|
1063
|
+
task = SecretsToEnv(
|
|
1064
|
+
project_config=project_config,
|
|
1065
|
+
task_config=task_config,
|
|
1066
|
+
org_config=None,
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
mock_provider = mock.Mock()
|
|
1070
|
+
mock_provider.provider_type = "local"
|
|
1071
|
+
mock_provider.get_credentials.return_value = "path\\file\nline\ttab\rreturn"
|
|
1072
|
+
task.provider = mock_provider
|
|
1073
|
+
|
|
1074
|
+
task()
|
|
1075
|
+
|
|
1076
|
+
# Verify safe_env_values has escaped characters
|
|
1077
|
+
safe_value = task.return_values["safe_env_values"]["COMPLEX_SECRET"]
|
|
1078
|
+
assert "\\\\" in safe_value # Escaped backslash
|
|
1079
|
+
assert "\\n" in safe_value # Escaped newline
|
|
1080
|
+
assert "\\t" in safe_value # Escaped tab
|
|
1081
|
+
assert "\\r" in safe_value # Escaped carriage return
|
|
1082
|
+
|
|
1083
|
+
def test_return_values_with_multiple_secrets(self):
|
|
1084
|
+
"""Test return_values with multiple secrets."""
|
|
1085
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1086
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1087
|
+
|
|
1088
|
+
task_config = TaskConfig(
|
|
1089
|
+
{
|
|
1090
|
+
"options": {
|
|
1091
|
+
"env_path": env_path,
|
|
1092
|
+
"secrets": ["KEY1", "KEY2", "KEY3"],
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
project_config = mock.Mock()
|
|
1098
|
+
project_config.repo_root = tmpdir
|
|
1099
|
+
|
|
1100
|
+
task = SecretsToEnv(
|
|
1101
|
+
project_config=project_config,
|
|
1102
|
+
task_config=task_config,
|
|
1103
|
+
org_config=None,
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
mock_provider = mock.Mock()
|
|
1107
|
+
mock_provider.provider_type = "local"
|
|
1108
|
+
mock_provider.get_credentials.side_effect = [
|
|
1109
|
+
"value1",
|
|
1110
|
+
'value with "quotes"',
|
|
1111
|
+
"value with\nnewlines",
|
|
1112
|
+
]
|
|
1113
|
+
task.provider = mock_provider
|
|
1114
|
+
|
|
1115
|
+
task()
|
|
1116
|
+
|
|
1117
|
+
# Verify all keys are in both env_values and safe_env_values
|
|
1118
|
+
assert len(task.return_values["env_values"]) == 3
|
|
1119
|
+
assert len(task.return_values["safe_env_values"]) == 3
|
|
1120
|
+
|
|
1121
|
+
# Verify original values in env_values
|
|
1122
|
+
assert task.return_values["env_values"]["KEY1"] == "value1"
|
|
1123
|
+
assert task.return_values["env_values"]["KEY2"] == 'value with "quotes"'
|
|
1124
|
+
assert task.return_values["env_values"]["KEY3"] == "value with\nnewlines"
|
|
1125
|
+
|
|
1126
|
+
# Verify escaped values in safe_env_values
|
|
1127
|
+
assert task.return_values["safe_env_values"]["KEY1"] == "value1"
|
|
1128
|
+
assert (
|
|
1129
|
+
task.return_values["safe_env_values"]["KEY2"]
|
|
1130
|
+
== 'value with \\"quotes\\"'
|
|
1131
|
+
)
|
|
1132
|
+
assert (
|
|
1133
|
+
task.return_values["safe_env_values"]["KEY3"] == "value with\\nnewlines"
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
def test_return_values_preserves_existing_env_values(self):
|
|
1137
|
+
"""Test that return_values includes existing env values."""
|
|
948
1138
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
949
1139
|
env_path = os.path.join(tmpdir, ".env")
|
|
950
1140
|
|
|
951
1141
|
# Create existing .env file
|
|
952
1142
|
with open(env_path, "w") as f:
|
|
953
|
-
f.write('
|
|
1143
|
+
f.write('EXISTING_KEY="existing_value"\n')
|
|
954
1144
|
|
|
955
1145
|
task_config = TaskConfig(
|
|
956
1146
|
{
|
|
957
1147
|
"options": {
|
|
958
1148
|
"env_path": env_path,
|
|
959
|
-
"secrets": [],
|
|
1149
|
+
"secrets": ["NEW_KEY"],
|
|
960
1150
|
}
|
|
961
1151
|
}
|
|
962
1152
|
)
|
|
@@ -970,20 +1160,25 @@ class TestSecretsToEnvEdgeCases:
|
|
|
970
1160
|
org_config=None,
|
|
971
1161
|
)
|
|
972
1162
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1163
|
+
mock_provider = mock.Mock()
|
|
1164
|
+
mock_provider.provider_type = "local"
|
|
1165
|
+
mock_provider.get_credentials.return_value = "new_value"
|
|
1166
|
+
task.provider = mock_provider
|
|
976
1167
|
|
|
977
1168
|
task()
|
|
978
1169
|
|
|
979
|
-
# Verify existing
|
|
980
|
-
|
|
981
|
-
|
|
1170
|
+
# Verify return_values contains both existing and new values
|
|
1171
|
+
assert "EXISTING_KEY" in task.return_values["env_values"]
|
|
1172
|
+
assert "NEW_KEY" in task.return_values["env_values"]
|
|
1173
|
+
assert task.return_values["env_values"]["EXISTING_KEY"] == "existing_value"
|
|
1174
|
+
assert task.return_values["env_values"]["NEW_KEY"] == "new_value"
|
|
982
1175
|
|
|
983
|
-
assert 'EXISTING="value"' in content
|
|
984
1176
|
|
|
985
|
-
|
|
986
|
-
|
|
1177
|
+
class TestSecretsToEnvFileFormat:
|
|
1178
|
+
"""Test cases for .env file format validation."""
|
|
1179
|
+
|
|
1180
|
+
def test_env_file_has_correct_line_format(self):
|
|
1181
|
+
"""Test that each line in .env file follows KEY="VALUE" format."""
|
|
987
1182
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
988
1183
|
env_path = os.path.join(tmpdir, ".env")
|
|
989
1184
|
|
|
@@ -991,7 +1186,7 @@ class TestSecretsToEnvEdgeCases:
|
|
|
991
1186
|
{
|
|
992
1187
|
"options": {
|
|
993
1188
|
"env_path": env_path,
|
|
994
|
-
"secrets": ["
|
|
1189
|
+
"secrets": ["KEY1", "KEY2", "KEY3"],
|
|
995
1190
|
}
|
|
996
1191
|
}
|
|
997
1192
|
)
|
|
@@ -1007,18 +1202,24 @@ class TestSecretsToEnvEdgeCases:
|
|
|
1007
1202
|
|
|
1008
1203
|
mock_provider = mock.Mock()
|
|
1009
1204
|
mock_provider.provider_type = "local"
|
|
1010
|
-
mock_provider.get_credentials.
|
|
1011
|
-
"value!@#$%^&*()[]{}|\\;':<>?,./~`"
|
|
1012
|
-
)
|
|
1205
|
+
mock_provider.get_credentials.side_effect = ["value1", "value2", "value3"]
|
|
1013
1206
|
task.provider = mock_provider
|
|
1014
1207
|
|
|
1015
1208
|
task()
|
|
1016
1209
|
|
|
1017
|
-
#
|
|
1018
|
-
|
|
1210
|
+
# Read raw file content
|
|
1211
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
|
1212
|
+
lines = f.readlines()
|
|
1019
1213
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1214
|
+
# Verify each line format
|
|
1215
|
+
import re
|
|
1216
|
+
|
|
1217
|
+
env_pattern = re.compile(r'^[A-Z_][A-Z0-9_]*=".*"\n$')
|
|
1218
|
+
for line in lines:
|
|
1219
|
+
assert env_pattern.match(line), f"Line does not match pattern: {line}"
|
|
1220
|
+
|
|
1221
|
+
def test_env_file_escapes_quotes_correctly(self):
|
|
1222
|
+
"""Test that quotes in values are escaped correctly in file."""
|
|
1022
1223
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1023
1224
|
env_path = os.path.join(tmpdir, ".env")
|
|
1024
1225
|
|
|
@@ -1026,7 +1227,7 @@ class TestSecretsToEnvEdgeCases:
|
|
|
1026
1227
|
{
|
|
1027
1228
|
"options": {
|
|
1028
1229
|
"env_path": env_path,
|
|
1029
|
-
"secrets": ["
|
|
1230
|
+
"secrets": ["QUOTE_KEY"],
|
|
1030
1231
|
}
|
|
1031
1232
|
}
|
|
1032
1233
|
)
|
|
@@ -1042,19 +1243,22 @@ class TestSecretsToEnvEdgeCases:
|
|
|
1042
1243
|
|
|
1043
1244
|
mock_provider = mock.Mock()
|
|
1044
1245
|
mock_provider.provider_type = "local"
|
|
1045
|
-
mock_provider.get_credentials.return_value =
|
|
1246
|
+
mock_provider.get_credentials.return_value = 'She said "Hello"'
|
|
1046
1247
|
task.provider = mock_provider
|
|
1047
1248
|
|
|
1048
1249
|
task()
|
|
1049
1250
|
|
|
1050
|
-
#
|
|
1251
|
+
# Read raw file content
|
|
1051
1252
|
with open(env_path, "r", encoding="utf-8") as f:
|
|
1052
1253
|
content = f.read()
|
|
1053
1254
|
|
|
1054
|
-
|
|
1255
|
+
# Verify quotes are escaped
|
|
1256
|
+
assert 'QUOTE_KEY="She said \\"Hello\\""' in content
|
|
1257
|
+
# Verify no unescaped quotes in the value
|
|
1258
|
+
assert 'She said "Hello"' not in content.split("=")[1]
|
|
1055
1259
|
|
|
1056
|
-
def
|
|
1057
|
-
"""Test
|
|
1260
|
+
def test_env_file_escapes_newlines_correctly(self):
|
|
1261
|
+
"""Test that newlines in values are escaped correctly in file."""
|
|
1058
1262
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1059
1263
|
env_path = os.path.join(tmpdir, ".env")
|
|
1060
1264
|
|
|
@@ -1062,7 +1266,7 @@ class TestSecretsToEnvEdgeCases:
|
|
|
1062
1266
|
{
|
|
1063
1267
|
"options": {
|
|
1064
1268
|
"env_path": env_path,
|
|
1065
|
-
"secrets": ["
|
|
1269
|
+
"secrets": ["MULTILINE_KEY"],
|
|
1066
1270
|
}
|
|
1067
1271
|
}
|
|
1068
1272
|
)
|
|
@@ -1078,14 +1282,988 @@ class TestSecretsToEnvEdgeCases:
|
|
|
1078
1282
|
|
|
1079
1283
|
mock_provider = mock.Mock()
|
|
1080
1284
|
mock_provider.provider_type = "local"
|
|
1081
|
-
|
|
1082
|
-
mock_provider.get_credentials.return_value = long_value
|
|
1285
|
+
mock_provider.get_credentials.return_value = "line1\nline2\nline3"
|
|
1083
1286
|
task.provider = mock_provider
|
|
1084
1287
|
|
|
1085
1288
|
task()
|
|
1086
1289
|
|
|
1087
|
-
#
|
|
1088
|
-
with open(env_path, "r") as f:
|
|
1290
|
+
# Read raw file content
|
|
1291
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
|
1292
|
+
lines = f.readlines()
|
|
1293
|
+
|
|
1294
|
+
# Should be only one line (newlines are escaped)
|
|
1295
|
+
assert len(lines) == 1
|
|
1296
|
+
# Verify newlines are escaped as \n
|
|
1297
|
+
assert "\\n" in lines[0]
|
|
1298
|
+
assert lines[0] == 'MULTILINE_KEY="line1\\nline2\\nline3"\n'
|
|
1299
|
+
|
|
1300
|
+
def test_env_file_escapes_backslashes_correctly(self):
|
|
1301
|
+
"""Test that backslashes in values are escaped correctly in file."""
|
|
1302
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1303
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1304
|
+
|
|
1305
|
+
task_config = TaskConfig(
|
|
1306
|
+
{
|
|
1307
|
+
"options": {
|
|
1308
|
+
"env_path": env_path,
|
|
1309
|
+
"secrets": ["PATH_KEY"],
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
)
|
|
1313
|
+
|
|
1314
|
+
project_config = mock.Mock()
|
|
1315
|
+
project_config.repo_root = tmpdir
|
|
1316
|
+
|
|
1317
|
+
task = SecretsToEnv(
|
|
1318
|
+
project_config=project_config,
|
|
1319
|
+
task_config=task_config,
|
|
1320
|
+
org_config=None,
|
|
1321
|
+
)
|
|
1322
|
+
|
|
1323
|
+
mock_provider = mock.Mock()
|
|
1324
|
+
mock_provider.provider_type = "local"
|
|
1325
|
+
mock_provider.get_credentials.return_value = "C:\\Users\\Admin"
|
|
1326
|
+
task.provider = mock_provider
|
|
1327
|
+
|
|
1328
|
+
task()
|
|
1329
|
+
|
|
1330
|
+
# Read raw file content
|
|
1331
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
|
1089
1332
|
content = f.read()
|
|
1090
1333
|
|
|
1091
|
-
|
|
1334
|
+
# Verify backslashes are escaped
|
|
1335
|
+
assert 'PATH_KEY="C:\\\\Users\\\\Admin"' in content
|
|
1336
|
+
|
|
1337
|
+
def test_env_file_loads_with_dotenv_values(self):
|
|
1338
|
+
"""Test that .env file can be loaded with dotenv_values."""
|
|
1339
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1340
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1341
|
+
|
|
1342
|
+
task_config = TaskConfig(
|
|
1343
|
+
{
|
|
1344
|
+
"options": {
|
|
1345
|
+
"env_path": env_path,
|
|
1346
|
+
"secrets": ["KEY1", "KEY2"],
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
project_config = mock.Mock()
|
|
1352
|
+
project_config.repo_root = tmpdir
|
|
1353
|
+
|
|
1354
|
+
task = SecretsToEnv(
|
|
1355
|
+
project_config=project_config,
|
|
1356
|
+
task_config=task_config,
|
|
1357
|
+
org_config=None,
|
|
1358
|
+
)
|
|
1359
|
+
|
|
1360
|
+
mock_provider = mock.Mock()
|
|
1361
|
+
mock_provider.provider_type = "local"
|
|
1362
|
+
mock_provider.get_credentials.side_effect = [
|
|
1363
|
+
'value with "quotes"',
|
|
1364
|
+
"value with\nnewlines",
|
|
1365
|
+
]
|
|
1366
|
+
task.provider = mock_provider
|
|
1367
|
+
|
|
1368
|
+
task()
|
|
1369
|
+
|
|
1370
|
+
# Load with dotenv_values and verify no errors
|
|
1371
|
+
from dotenv import dotenv_values
|
|
1372
|
+
|
|
1373
|
+
loaded_values = dotenv_values(env_path)
|
|
1374
|
+
assert loaded_values["KEY1"] == 'value with "quotes"'
|
|
1375
|
+
assert loaded_values["KEY2"] == "value with\nnewlines"
|
|
1376
|
+
|
|
1377
|
+
def test_env_file_loads_with_load_dotenv(self):
|
|
1378
|
+
"""Test that .env file can be loaded with load_dotenv."""
|
|
1379
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1380
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1381
|
+
|
|
1382
|
+
task_config = TaskConfig(
|
|
1383
|
+
{
|
|
1384
|
+
"options": {
|
|
1385
|
+
"env_path": env_path,
|
|
1386
|
+
"secrets": ["TEST_KEY1", "TEST_KEY2"],
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
)
|
|
1390
|
+
|
|
1391
|
+
project_config = mock.Mock()
|
|
1392
|
+
project_config.repo_root = tmpdir
|
|
1393
|
+
|
|
1394
|
+
task = SecretsToEnv(
|
|
1395
|
+
project_config=project_config,
|
|
1396
|
+
task_config=task_config,
|
|
1397
|
+
org_config=None,
|
|
1398
|
+
)
|
|
1399
|
+
|
|
1400
|
+
mock_provider = mock.Mock()
|
|
1401
|
+
mock_provider.provider_type = "local"
|
|
1402
|
+
mock_provider.get_credentials.side_effect = [
|
|
1403
|
+
"value with \"quotes\" and 'single quotes'",
|
|
1404
|
+
"value with\ttabs and\nnewlines",
|
|
1405
|
+
]
|
|
1406
|
+
task.provider = mock_provider
|
|
1407
|
+
|
|
1408
|
+
task()
|
|
1409
|
+
|
|
1410
|
+
# Load with load_dotenv and verify no errors
|
|
1411
|
+
from dotenv import load_dotenv
|
|
1412
|
+
|
|
1413
|
+
# Clear any existing env vars
|
|
1414
|
+
for key in ["TEST_KEY1", "TEST_KEY2"]:
|
|
1415
|
+
os.environ.pop(key, None)
|
|
1416
|
+
|
|
1417
|
+
# Load the .env file
|
|
1418
|
+
load_dotenv(env_path)
|
|
1419
|
+
|
|
1420
|
+
# Verify values are loaded correctly
|
|
1421
|
+
assert (
|
|
1422
|
+
os.environ["TEST_KEY1"] == "value with \"quotes\" and 'single quotes'"
|
|
1423
|
+
)
|
|
1424
|
+
assert os.environ["TEST_KEY2"] == "value with\ttabs and\nnewlines"
|
|
1425
|
+
|
|
1426
|
+
# Cleanup
|
|
1427
|
+
del os.environ["TEST_KEY1"]
|
|
1428
|
+
del os.environ["TEST_KEY2"]
|
|
1429
|
+
|
|
1430
|
+
def test_env_file_with_all_escape_characters_loads_correctly(self):
|
|
1431
|
+
"""Test that file with all escape characters can be loaded correctly."""
|
|
1432
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1433
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1434
|
+
|
|
1435
|
+
task_config = TaskConfig(
|
|
1436
|
+
{
|
|
1437
|
+
"options": {
|
|
1438
|
+
"env_path": env_path,
|
|
1439
|
+
"secrets": ["ALL_ESCAPES"],
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1444
|
+
project_config = mock.Mock()
|
|
1445
|
+
project_config.repo_root = tmpdir
|
|
1446
|
+
|
|
1447
|
+
task = SecretsToEnv(
|
|
1448
|
+
project_config=project_config,
|
|
1449
|
+
task_config=task_config,
|
|
1450
|
+
org_config=None,
|
|
1451
|
+
)
|
|
1452
|
+
|
|
1453
|
+
mock_provider = mock.Mock()
|
|
1454
|
+
mock_provider.provider_type = "local"
|
|
1455
|
+
# Test all escape characters
|
|
1456
|
+
test_value = "bs\\ dq\" sq' bell\a back\b ff\f nl\n cr\r tab\t vt\v"
|
|
1457
|
+
mock_provider.get_credentials.return_value = test_value
|
|
1458
|
+
task.provider = mock_provider
|
|
1459
|
+
|
|
1460
|
+
task()
|
|
1461
|
+
|
|
1462
|
+
# Verify file was written
|
|
1463
|
+
assert os.path.exists(env_path)
|
|
1464
|
+
|
|
1465
|
+
# Load with dotenv_values and verify
|
|
1466
|
+
from dotenv import dotenv_values
|
|
1467
|
+
|
|
1468
|
+
loaded_values = dotenv_values(env_path)
|
|
1469
|
+
assert "ALL_ESCAPES" in loaded_values
|
|
1470
|
+
assert loaded_values["ALL_ESCAPES"] == test_value
|
|
1471
|
+
|
|
1472
|
+
def test_env_file_utf8_encoding(self):
|
|
1473
|
+
"""Test that .env file is written with UTF-8 encoding."""
|
|
1474
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1475
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1476
|
+
|
|
1477
|
+
task_config = TaskConfig(
|
|
1478
|
+
{
|
|
1479
|
+
"options": {
|
|
1480
|
+
"env_path": env_path,
|
|
1481
|
+
"secrets": ["UNICODE_KEY"],
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
)
|
|
1485
|
+
|
|
1486
|
+
project_config = mock.Mock()
|
|
1487
|
+
project_config.repo_root = tmpdir
|
|
1488
|
+
|
|
1489
|
+
task = SecretsToEnv(
|
|
1490
|
+
project_config=project_config,
|
|
1491
|
+
task_config=task_config,
|
|
1492
|
+
org_config=None,
|
|
1493
|
+
)
|
|
1494
|
+
|
|
1495
|
+
mock_provider = mock.Mock()
|
|
1496
|
+
mock_provider.provider_type = "local"
|
|
1497
|
+
mock_provider.get_credentials.return_value = "Hello 世界 🌍 مرحبا"
|
|
1498
|
+
task.provider = mock_provider
|
|
1499
|
+
|
|
1500
|
+
task()
|
|
1501
|
+
|
|
1502
|
+
# Read file with UTF-8 encoding
|
|
1503
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
|
1504
|
+
content = f.read()
|
|
1505
|
+
|
|
1506
|
+
assert "Hello 世界 🌍 مرحبا" in content
|
|
1507
|
+
|
|
1508
|
+
# Load with dotenv_values and verify unicode is preserved
|
|
1509
|
+
from dotenv import dotenv_values
|
|
1510
|
+
|
|
1511
|
+
loaded_values = dotenv_values(env_path)
|
|
1512
|
+
assert loaded_values["UNICODE_KEY"] == "Hello 世界 🌍 مرحبا"
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
class TestSecretsToEnvEdgeCases:
|
|
1516
|
+
"""Test edge cases and error conditions."""
|
|
1517
|
+
|
|
1518
|
+
def test_empty_secrets_list_creates_empty_env_file(self):
|
|
1519
|
+
"""Test that empty secrets list still creates/updates env file."""
|
|
1520
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1521
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1522
|
+
|
|
1523
|
+
# Create existing .env file
|
|
1524
|
+
with open(env_path, "w") as f:
|
|
1525
|
+
f.write('EXISTING="value"\n')
|
|
1526
|
+
|
|
1527
|
+
task_config = TaskConfig(
|
|
1528
|
+
{
|
|
1529
|
+
"options": {
|
|
1530
|
+
"env_path": env_path,
|
|
1531
|
+
"secrets": [],
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
)
|
|
1535
|
+
|
|
1536
|
+
project_config = mock.Mock()
|
|
1537
|
+
project_config.repo_root = tmpdir
|
|
1538
|
+
|
|
1539
|
+
task = SecretsToEnv(
|
|
1540
|
+
project_config=project_config,
|
|
1541
|
+
task_config=task_config,
|
|
1542
|
+
org_config=None,
|
|
1543
|
+
)
|
|
1544
|
+
|
|
1545
|
+
# When secrets is an empty list, _init_secrets doesn't set task.secrets
|
|
1546
|
+
# so we need to set it manually for this test
|
|
1547
|
+
task.secrets = {}
|
|
1548
|
+
|
|
1549
|
+
task()
|
|
1550
|
+
|
|
1551
|
+
# Verify existing content is preserved
|
|
1552
|
+
with open(env_path, "r") as f:
|
|
1553
|
+
content = f.read()
|
|
1554
|
+
|
|
1555
|
+
assert 'EXISTING="value"' in content
|
|
1556
|
+
|
|
1557
|
+
def test_special_characters_in_secret_values(self):
|
|
1558
|
+
"""Test handling of special characters in secret values."""
|
|
1559
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1560
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1561
|
+
|
|
1562
|
+
task_config = TaskConfig(
|
|
1563
|
+
{
|
|
1564
|
+
"options": {
|
|
1565
|
+
"env_path": env_path,
|
|
1566
|
+
"secrets": ["SPECIAL"],
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
)
|
|
1570
|
+
|
|
1571
|
+
project_config = mock.Mock()
|
|
1572
|
+
project_config.repo_root = tmpdir
|
|
1573
|
+
|
|
1574
|
+
task = SecretsToEnv(
|
|
1575
|
+
project_config=project_config,
|
|
1576
|
+
task_config=task_config,
|
|
1577
|
+
org_config=None,
|
|
1578
|
+
)
|
|
1579
|
+
|
|
1580
|
+
mock_provider = mock.Mock()
|
|
1581
|
+
mock_provider.provider_type = "local"
|
|
1582
|
+
mock_provider.get_credentials.return_value = (
|
|
1583
|
+
"value!@#$%^&*()[]{}|\\;':<>?,./~`"
|
|
1584
|
+
)
|
|
1585
|
+
task.provider = mock_provider
|
|
1586
|
+
|
|
1587
|
+
task()
|
|
1588
|
+
|
|
1589
|
+
# Verify file can be read
|
|
1590
|
+
assert os.path.exists(env_path)
|
|
1591
|
+
|
|
1592
|
+
def test_unicode_characters_in_secret_values(self):
|
|
1593
|
+
"""Test handling of unicode characters in secret values."""
|
|
1594
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1595
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1596
|
+
|
|
1597
|
+
task_config = TaskConfig(
|
|
1598
|
+
{
|
|
1599
|
+
"options": {
|
|
1600
|
+
"env_path": env_path,
|
|
1601
|
+
"secrets": ["UNICODE"],
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
)
|
|
1605
|
+
|
|
1606
|
+
project_config = mock.Mock()
|
|
1607
|
+
project_config.repo_root = tmpdir
|
|
1608
|
+
|
|
1609
|
+
task = SecretsToEnv(
|
|
1610
|
+
project_config=project_config,
|
|
1611
|
+
task_config=task_config,
|
|
1612
|
+
org_config=None,
|
|
1613
|
+
)
|
|
1614
|
+
|
|
1615
|
+
mock_provider = mock.Mock()
|
|
1616
|
+
mock_provider.provider_type = "local"
|
|
1617
|
+
mock_provider.get_credentials.return_value = "Hello 世界 🌍 Привет"
|
|
1618
|
+
task.provider = mock_provider
|
|
1619
|
+
|
|
1620
|
+
task()
|
|
1621
|
+
|
|
1622
|
+
# Verify file contents
|
|
1623
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
|
1624
|
+
content = f.read()
|
|
1625
|
+
|
|
1626
|
+
assert 'UNICODE="Hello 世界 🌍 Привет"' in content
|
|
1627
|
+
|
|
1628
|
+
def test_very_long_secret_value(self):
|
|
1629
|
+
"""Test handling of very long secret values."""
|
|
1630
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1631
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1632
|
+
|
|
1633
|
+
task_config = TaskConfig(
|
|
1634
|
+
{
|
|
1635
|
+
"options": {
|
|
1636
|
+
"env_path": env_path,
|
|
1637
|
+
"secrets": ["LONG_SECRET"],
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
)
|
|
1641
|
+
|
|
1642
|
+
project_config = mock.Mock()
|
|
1643
|
+
project_config.repo_root = tmpdir
|
|
1644
|
+
|
|
1645
|
+
task = SecretsToEnv(
|
|
1646
|
+
project_config=project_config,
|
|
1647
|
+
task_config=task_config,
|
|
1648
|
+
org_config=None,
|
|
1649
|
+
)
|
|
1650
|
+
|
|
1651
|
+
mock_provider = mock.Mock()
|
|
1652
|
+
mock_provider.provider_type = "local"
|
|
1653
|
+
long_value = "x" * 10000 # 10k characters
|
|
1654
|
+
mock_provider.get_credentials.return_value = long_value
|
|
1655
|
+
task.provider = mock_provider
|
|
1656
|
+
|
|
1657
|
+
task()
|
|
1658
|
+
|
|
1659
|
+
# Verify file contents
|
|
1660
|
+
with open(env_path, "r") as f:
|
|
1661
|
+
content = f.read()
|
|
1662
|
+
|
|
1663
|
+
assert f'LONG_SECRET="{long_value}"' in content
|
|
1664
|
+
|
|
1665
|
+
|
|
1666
|
+
class TestSecretsToEnvEscapeValues:
|
|
1667
|
+
"""Test cases for _escape_env_value method."""
|
|
1668
|
+
|
|
1669
|
+
def test_escape_backslash(self):
|
|
1670
|
+
"""Test escaping backslash characters."""
|
|
1671
|
+
task_config = TaskConfig(
|
|
1672
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
1673
|
+
)
|
|
1674
|
+
|
|
1675
|
+
task = SecretsToEnv(
|
|
1676
|
+
project_config=mock.Mock(),
|
|
1677
|
+
task_config=task_config,
|
|
1678
|
+
org_config=None,
|
|
1679
|
+
)
|
|
1680
|
+
|
|
1681
|
+
result = task._escape_env_value("path\\to\\file")
|
|
1682
|
+
assert result == "path\\\\to\\\\file"
|
|
1683
|
+
|
|
1684
|
+
def test_escape_double_quotes(self):
|
|
1685
|
+
"""Test escaping double quote characters."""
|
|
1686
|
+
task_config = TaskConfig(
|
|
1687
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
1688
|
+
)
|
|
1689
|
+
|
|
1690
|
+
task = SecretsToEnv(
|
|
1691
|
+
project_config=mock.Mock(),
|
|
1692
|
+
task_config=task_config,
|
|
1693
|
+
org_config=None,
|
|
1694
|
+
)
|
|
1695
|
+
|
|
1696
|
+
result = task._escape_env_value('value with "quotes"')
|
|
1697
|
+
assert result == 'value with \\"quotes\\"'
|
|
1698
|
+
|
|
1699
|
+
def test_escape_single_quotes(self):
|
|
1700
|
+
"""Test escaping single quote characters."""
|
|
1701
|
+
task_config = TaskConfig(
|
|
1702
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
1703
|
+
)
|
|
1704
|
+
|
|
1705
|
+
task = SecretsToEnv(
|
|
1706
|
+
project_config=mock.Mock(),
|
|
1707
|
+
task_config=task_config,
|
|
1708
|
+
org_config=None,
|
|
1709
|
+
)
|
|
1710
|
+
|
|
1711
|
+
result = task._escape_env_value("value with 'quotes'")
|
|
1712
|
+
assert result == "value with \\'quotes\\'"
|
|
1713
|
+
|
|
1714
|
+
def test_escape_newline(self):
|
|
1715
|
+
"""Test escaping newline characters."""
|
|
1716
|
+
task_config = TaskConfig(
|
|
1717
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
1718
|
+
)
|
|
1719
|
+
|
|
1720
|
+
task = SecretsToEnv(
|
|
1721
|
+
project_config=mock.Mock(),
|
|
1722
|
+
task_config=task_config,
|
|
1723
|
+
org_config=None,
|
|
1724
|
+
)
|
|
1725
|
+
|
|
1726
|
+
result = task._escape_env_value("line1\nline2")
|
|
1727
|
+
assert result == "line1\\nline2"
|
|
1728
|
+
|
|
1729
|
+
def test_escape_carriage_return(self):
|
|
1730
|
+
"""Test escaping carriage return characters."""
|
|
1731
|
+
task_config = TaskConfig(
|
|
1732
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
1733
|
+
)
|
|
1734
|
+
|
|
1735
|
+
task = SecretsToEnv(
|
|
1736
|
+
project_config=mock.Mock(),
|
|
1737
|
+
task_config=task_config,
|
|
1738
|
+
org_config=None,
|
|
1739
|
+
)
|
|
1740
|
+
|
|
1741
|
+
result = task._escape_env_value("line1\rline2")
|
|
1742
|
+
assert result == "line1\\rline2"
|
|
1743
|
+
|
|
1744
|
+
def test_escape_tab(self):
|
|
1745
|
+
"""Test escaping tab characters."""
|
|
1746
|
+
task_config = TaskConfig(
|
|
1747
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
task = SecretsToEnv(
|
|
1751
|
+
project_config=mock.Mock(),
|
|
1752
|
+
task_config=task_config,
|
|
1753
|
+
org_config=None,
|
|
1754
|
+
)
|
|
1755
|
+
|
|
1756
|
+
result = task._escape_env_value("col1\tcol2")
|
|
1757
|
+
assert result == "col1\\tcol2"
|
|
1758
|
+
|
|
1759
|
+
def test_escape_bell(self):
|
|
1760
|
+
"""Test escaping bell/alert characters."""
|
|
1761
|
+
task_config = TaskConfig(
|
|
1762
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
1763
|
+
)
|
|
1764
|
+
|
|
1765
|
+
task = SecretsToEnv(
|
|
1766
|
+
project_config=mock.Mock(),
|
|
1767
|
+
task_config=task_config,
|
|
1768
|
+
org_config=None,
|
|
1769
|
+
)
|
|
1770
|
+
|
|
1771
|
+
result = task._escape_env_value("text\abell")
|
|
1772
|
+
assert result == "text\\abell"
|
|
1773
|
+
|
|
1774
|
+
def test_escape_backspace(self):
|
|
1775
|
+
"""Test escaping backspace characters."""
|
|
1776
|
+
task_config = TaskConfig(
|
|
1777
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
1778
|
+
)
|
|
1779
|
+
|
|
1780
|
+
task = SecretsToEnv(
|
|
1781
|
+
project_config=mock.Mock(),
|
|
1782
|
+
task_config=task_config,
|
|
1783
|
+
org_config=None,
|
|
1784
|
+
)
|
|
1785
|
+
|
|
1786
|
+
result = task._escape_env_value("text\bback")
|
|
1787
|
+
assert result == "text\\bback"
|
|
1788
|
+
|
|
1789
|
+
def test_escape_form_feed(self):
|
|
1790
|
+
"""Test escaping form feed characters."""
|
|
1791
|
+
task_config = TaskConfig(
|
|
1792
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
1793
|
+
)
|
|
1794
|
+
|
|
1795
|
+
task = SecretsToEnv(
|
|
1796
|
+
project_config=mock.Mock(),
|
|
1797
|
+
task_config=task_config,
|
|
1798
|
+
org_config=None,
|
|
1799
|
+
)
|
|
1800
|
+
|
|
1801
|
+
result = task._escape_env_value("text\fform")
|
|
1802
|
+
assert result == "text\\fform"
|
|
1803
|
+
|
|
1804
|
+
def test_escape_vertical_tab(self):
|
|
1805
|
+
"""Test escaping vertical tab characters."""
|
|
1806
|
+
task_config = TaskConfig(
|
|
1807
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
1808
|
+
)
|
|
1809
|
+
|
|
1810
|
+
task = SecretsToEnv(
|
|
1811
|
+
project_config=mock.Mock(),
|
|
1812
|
+
task_config=task_config,
|
|
1813
|
+
org_config=None,
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1816
|
+
result = task._escape_env_value("text\vvertical")
|
|
1817
|
+
assert result == "text\\vvertical"
|
|
1818
|
+
|
|
1819
|
+
def test_escape_multiple_special_chars(self):
|
|
1820
|
+
"""Test escaping multiple special characters together."""
|
|
1821
|
+
task_config = TaskConfig(
|
|
1822
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
1823
|
+
)
|
|
1824
|
+
|
|
1825
|
+
task = SecretsToEnv(
|
|
1826
|
+
project_config=mock.Mock(),
|
|
1827
|
+
task_config=task_config,
|
|
1828
|
+
org_config=None,
|
|
1829
|
+
)
|
|
1830
|
+
|
|
1831
|
+
result = task._escape_env_value('path\\to\\"file"\nwith\ttabs')
|
|
1832
|
+
assert result == 'path\\\\to\\\\\\"file\\"\\nwith\\ttabs'
|
|
1833
|
+
|
|
1834
|
+
def test_escape_non_string_value(self):
|
|
1835
|
+
"""Test that non-string values are returned unchanged."""
|
|
1836
|
+
task_config = TaskConfig(
|
|
1837
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
1838
|
+
)
|
|
1839
|
+
|
|
1840
|
+
task = SecretsToEnv(
|
|
1841
|
+
project_config=mock.Mock(),
|
|
1842
|
+
task_config=task_config,
|
|
1843
|
+
org_config=None,
|
|
1844
|
+
)
|
|
1845
|
+
|
|
1846
|
+
# Test with None
|
|
1847
|
+
assert task._escape_env_value(None) is None
|
|
1848
|
+
|
|
1849
|
+
# Test with number
|
|
1850
|
+
assert task._escape_env_value(123) == 123
|
|
1851
|
+
|
|
1852
|
+
# Test with boolean
|
|
1853
|
+
assert task._escape_env_value(True) is True
|
|
1854
|
+
|
|
1855
|
+
def test_escape_empty_string(self):
|
|
1856
|
+
"""Test escaping empty string."""
|
|
1857
|
+
task_config = TaskConfig(
|
|
1858
|
+
{"options": {"env_path": ".env", "secrets": ["TEST_SECRET"]}}
|
|
1859
|
+
)
|
|
1860
|
+
|
|
1861
|
+
task = SecretsToEnv(
|
|
1862
|
+
project_config=mock.Mock(),
|
|
1863
|
+
task_config=task_config,
|
|
1864
|
+
org_config=None,
|
|
1865
|
+
)
|
|
1866
|
+
|
|
1867
|
+
result = task._escape_env_value("")
|
|
1868
|
+
assert result == ""
|
|
1869
|
+
|
|
1870
|
+
|
|
1871
|
+
class TestSecretsToEnvRoundTrip:
|
|
1872
|
+
"""Test cases for round-trip write and load of .env files."""
|
|
1873
|
+
|
|
1874
|
+
def test_roundtrip_with_quotes(self):
|
|
1875
|
+
"""Test writing and loading .env file with quoted values."""
|
|
1876
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1877
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1878
|
+
|
|
1879
|
+
task_config = TaskConfig(
|
|
1880
|
+
{
|
|
1881
|
+
"options": {
|
|
1882
|
+
"env_path": env_path,
|
|
1883
|
+
"secrets": ["QUOTE_SECRET"],
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
)
|
|
1887
|
+
|
|
1888
|
+
project_config = mock.Mock()
|
|
1889
|
+
project_config.repo_root = tmpdir
|
|
1890
|
+
|
|
1891
|
+
task = SecretsToEnv(
|
|
1892
|
+
project_config=project_config,
|
|
1893
|
+
task_config=task_config,
|
|
1894
|
+
org_config=None,
|
|
1895
|
+
)
|
|
1896
|
+
|
|
1897
|
+
mock_provider = mock.Mock()
|
|
1898
|
+
mock_provider.provider_type = "local"
|
|
1899
|
+
mock_provider.get_credentials.return_value = 'value with "quotes"'
|
|
1900
|
+
task.provider = mock_provider
|
|
1901
|
+
|
|
1902
|
+
task()
|
|
1903
|
+
|
|
1904
|
+
# Load the file back and verify
|
|
1905
|
+
from dotenv import dotenv_values
|
|
1906
|
+
|
|
1907
|
+
loaded_values = dotenv_values(env_path)
|
|
1908
|
+
assert "QUOTE_SECRET" in loaded_values
|
|
1909
|
+
assert loaded_values["QUOTE_SECRET"] == 'value with "quotes"'
|
|
1910
|
+
|
|
1911
|
+
def test_roundtrip_with_newlines(self):
|
|
1912
|
+
"""Test writing and loading .env file with newline values."""
|
|
1913
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1914
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1915
|
+
|
|
1916
|
+
task_config = TaskConfig(
|
|
1917
|
+
{
|
|
1918
|
+
"options": {
|
|
1919
|
+
"env_path": env_path,
|
|
1920
|
+
"secrets": ["MULTILINE_SECRET"],
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
)
|
|
1924
|
+
|
|
1925
|
+
project_config = mock.Mock()
|
|
1926
|
+
project_config.repo_root = tmpdir
|
|
1927
|
+
|
|
1928
|
+
task = SecretsToEnv(
|
|
1929
|
+
project_config=project_config,
|
|
1930
|
+
task_config=task_config,
|
|
1931
|
+
org_config=None,
|
|
1932
|
+
)
|
|
1933
|
+
|
|
1934
|
+
mock_provider = mock.Mock()
|
|
1935
|
+
mock_provider.provider_type = "local"
|
|
1936
|
+
mock_provider.get_credentials.return_value = "line1\nline2\nline3"
|
|
1937
|
+
task.provider = mock_provider
|
|
1938
|
+
|
|
1939
|
+
task()
|
|
1940
|
+
|
|
1941
|
+
# Load the file back and verify
|
|
1942
|
+
from dotenv import dotenv_values
|
|
1943
|
+
|
|
1944
|
+
loaded_values = dotenv_values(env_path)
|
|
1945
|
+
assert "MULTILINE_SECRET" in loaded_values
|
|
1946
|
+
assert loaded_values["MULTILINE_SECRET"] == "line1\nline2\nline3"
|
|
1947
|
+
|
|
1948
|
+
def test_roundtrip_with_backslashes(self):
|
|
1949
|
+
"""Test writing and loading .env file with backslash values."""
|
|
1950
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1951
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1952
|
+
|
|
1953
|
+
task_config = TaskConfig(
|
|
1954
|
+
{
|
|
1955
|
+
"options": {
|
|
1956
|
+
"env_path": env_path,
|
|
1957
|
+
"secrets": ["PATH_SECRET"],
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
)
|
|
1961
|
+
|
|
1962
|
+
project_config = mock.Mock()
|
|
1963
|
+
project_config.repo_root = tmpdir
|
|
1964
|
+
|
|
1965
|
+
task = SecretsToEnv(
|
|
1966
|
+
project_config=project_config,
|
|
1967
|
+
task_config=task_config,
|
|
1968
|
+
org_config=None,
|
|
1969
|
+
)
|
|
1970
|
+
|
|
1971
|
+
mock_provider = mock.Mock()
|
|
1972
|
+
mock_provider.provider_type = "local"
|
|
1973
|
+
mock_provider.get_credentials.return_value = "C:\\Users\\Admin\\Documents"
|
|
1974
|
+
task.provider = mock_provider
|
|
1975
|
+
|
|
1976
|
+
task()
|
|
1977
|
+
|
|
1978
|
+
# Load the file back and verify
|
|
1979
|
+
from dotenv import dotenv_values
|
|
1980
|
+
|
|
1981
|
+
loaded_values = dotenv_values(env_path)
|
|
1982
|
+
assert "PATH_SECRET" in loaded_values
|
|
1983
|
+
assert loaded_values["PATH_SECRET"] == "C:\\Users\\Admin\\Documents"
|
|
1984
|
+
|
|
1985
|
+
def test_roundtrip_with_tabs(self):
|
|
1986
|
+
"""Test writing and loading .env file with tab values."""
|
|
1987
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1988
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
1989
|
+
|
|
1990
|
+
task_config = TaskConfig(
|
|
1991
|
+
{
|
|
1992
|
+
"options": {
|
|
1993
|
+
"env_path": env_path,
|
|
1994
|
+
"secrets": ["TAB_SECRET"],
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
)
|
|
1998
|
+
|
|
1999
|
+
project_config = mock.Mock()
|
|
2000
|
+
project_config.repo_root = tmpdir
|
|
2001
|
+
|
|
2002
|
+
task = SecretsToEnv(
|
|
2003
|
+
project_config=project_config,
|
|
2004
|
+
task_config=task_config,
|
|
2005
|
+
org_config=None,
|
|
2006
|
+
)
|
|
2007
|
+
|
|
2008
|
+
mock_provider = mock.Mock()
|
|
2009
|
+
mock_provider.provider_type = "local"
|
|
2010
|
+
mock_provider.get_credentials.return_value = "col1\tcol2\tcol3"
|
|
2011
|
+
task.provider = mock_provider
|
|
2012
|
+
|
|
2013
|
+
task()
|
|
2014
|
+
|
|
2015
|
+
# Load the file back and verify
|
|
2016
|
+
from dotenv import dotenv_values
|
|
2017
|
+
|
|
2018
|
+
loaded_values = dotenv_values(env_path)
|
|
2019
|
+
assert "TAB_SECRET" in loaded_values
|
|
2020
|
+
assert loaded_values["TAB_SECRET"] == "col1\tcol2\tcol3"
|
|
2021
|
+
|
|
2022
|
+
def test_roundtrip_with_mixed_special_chars(self):
|
|
2023
|
+
"""Test writing and loading .env file with mixed special characters."""
|
|
2024
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
2025
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
2026
|
+
|
|
2027
|
+
task_config = TaskConfig(
|
|
2028
|
+
{
|
|
2029
|
+
"options": {
|
|
2030
|
+
"env_path": env_path,
|
|
2031
|
+
"secrets": ["COMPLEX_SECRET"],
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
)
|
|
2035
|
+
|
|
2036
|
+
project_config = mock.Mock()
|
|
2037
|
+
project_config.repo_root = tmpdir
|
|
2038
|
+
|
|
2039
|
+
task = SecretsToEnv(
|
|
2040
|
+
project_config=project_config,
|
|
2041
|
+
task_config=task_config,
|
|
2042
|
+
org_config=None,
|
|
2043
|
+
)
|
|
2044
|
+
|
|
2045
|
+
mock_provider = mock.Mock()
|
|
2046
|
+
mock_provider.provider_type = "local"
|
|
2047
|
+
complex_value = "path\\to\\\"file\"\nwith\ttabs\rand'quotes'"
|
|
2048
|
+
mock_provider.get_credentials.return_value = complex_value
|
|
2049
|
+
task.provider = mock_provider
|
|
2050
|
+
|
|
2051
|
+
task()
|
|
2052
|
+
|
|
2053
|
+
# Load the file back and verify
|
|
2054
|
+
from dotenv import dotenv_values
|
|
2055
|
+
|
|
2056
|
+
loaded_values = dotenv_values(env_path)
|
|
2057
|
+
assert "COMPLEX_SECRET" in loaded_values
|
|
2058
|
+
assert loaded_values["COMPLEX_SECRET"] == complex_value
|
|
2059
|
+
|
|
2060
|
+
def test_roundtrip_with_json_string(self):
|
|
2061
|
+
"""Test writing and loading .env file with JSON string values."""
|
|
2062
|
+
import json
|
|
2063
|
+
|
|
2064
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
2065
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
2066
|
+
|
|
2067
|
+
task_config = TaskConfig(
|
|
2068
|
+
{
|
|
2069
|
+
"options": {
|
|
2070
|
+
"env_path": env_path,
|
|
2071
|
+
"secrets": ["JSON_SECRET"],
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
)
|
|
2075
|
+
|
|
2076
|
+
project_config = mock.Mock()
|
|
2077
|
+
project_config.repo_root = tmpdir
|
|
2078
|
+
|
|
2079
|
+
task = SecretsToEnv(
|
|
2080
|
+
project_config=project_config,
|
|
2081
|
+
task_config=task_config,
|
|
2082
|
+
org_config=None,
|
|
2083
|
+
)
|
|
2084
|
+
|
|
2085
|
+
mock_provider = mock.Mock()
|
|
2086
|
+
mock_provider.provider_type = "local"
|
|
2087
|
+
json_value = json.dumps({"key": "value", "nested": {"data": "test"}})
|
|
2088
|
+
mock_provider.get_credentials.return_value = json_value
|
|
2089
|
+
task.provider = mock_provider
|
|
2090
|
+
|
|
2091
|
+
task()
|
|
2092
|
+
|
|
2093
|
+
# Load the file back and verify
|
|
2094
|
+
from dotenv import dotenv_values
|
|
2095
|
+
|
|
2096
|
+
loaded_values = dotenv_values(env_path)
|
|
2097
|
+
assert "JSON_SECRET" in loaded_values
|
|
2098
|
+
assert loaded_values["JSON_SECRET"] == json_value
|
|
2099
|
+
# Verify JSON can be parsed
|
|
2100
|
+
assert json.loads(loaded_values["JSON_SECRET"]) == {
|
|
2101
|
+
"key": "value",
|
|
2102
|
+
"nested": {"data": "test"},
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
def test_roundtrip_with_multiple_secrets(self):
|
|
2106
|
+
"""Test writing and loading .env file with multiple secrets."""
|
|
2107
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
2108
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
2109
|
+
|
|
2110
|
+
task_config = TaskConfig(
|
|
2111
|
+
{
|
|
2112
|
+
"options": {
|
|
2113
|
+
"env_path": env_path,
|
|
2114
|
+
"secrets": ["SECRET1", "SECRET2", "SECRET3"],
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
)
|
|
2118
|
+
|
|
2119
|
+
project_config = mock.Mock()
|
|
2120
|
+
project_config.repo_root = tmpdir
|
|
2121
|
+
|
|
2122
|
+
task = SecretsToEnv(
|
|
2123
|
+
project_config=project_config,
|
|
2124
|
+
task_config=task_config,
|
|
2125
|
+
org_config=None,
|
|
2126
|
+
)
|
|
2127
|
+
|
|
2128
|
+
mock_provider = mock.Mock()
|
|
2129
|
+
mock_provider.provider_type = "local"
|
|
2130
|
+
mock_provider.get_credentials.side_effect = [
|
|
2131
|
+
'value with "quotes"',
|
|
2132
|
+
"line1\nline2",
|
|
2133
|
+
"path\\to\\file",
|
|
2134
|
+
]
|
|
2135
|
+
task.provider = mock_provider
|
|
2136
|
+
|
|
2137
|
+
task()
|
|
2138
|
+
|
|
2139
|
+
# Load the file back and verify
|
|
2140
|
+
from dotenv import dotenv_values
|
|
2141
|
+
|
|
2142
|
+
loaded_values = dotenv_values(env_path)
|
|
2143
|
+
assert loaded_values["SECRET1"] == 'value with "quotes"'
|
|
2144
|
+
assert loaded_values["SECRET2"] == "line1\nline2"
|
|
2145
|
+
assert loaded_values["SECRET3"] == "path\\to\\file"
|
|
2146
|
+
|
|
2147
|
+
def test_roundtrip_preserves_all_escape_chars(self):
|
|
2148
|
+
"""Test that all escape characters are preserved in round-trip."""
|
|
2149
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
2150
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
2151
|
+
|
|
2152
|
+
task_config = TaskConfig(
|
|
2153
|
+
{
|
|
2154
|
+
"options": {
|
|
2155
|
+
"env_path": env_path,
|
|
2156
|
+
"secrets": ["ALL_ESCAPES"],
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
)
|
|
2160
|
+
|
|
2161
|
+
project_config = mock.Mock()
|
|
2162
|
+
project_config.repo_root = tmpdir
|
|
2163
|
+
|
|
2164
|
+
task = SecretsToEnv(
|
|
2165
|
+
project_config=project_config,
|
|
2166
|
+
task_config=task_config,
|
|
2167
|
+
org_config=None,
|
|
2168
|
+
)
|
|
2169
|
+
|
|
2170
|
+
mock_provider = mock.Mock()
|
|
2171
|
+
mock_provider.provider_type = "local"
|
|
2172
|
+
# Test all escape characters: \\ \' \" \a \b \f \n \r \t \v
|
|
2173
|
+
test_value = "backslash\\ quote\" single' bell\a back\b form\f new\n ret\r tab\t vert\v"
|
|
2174
|
+
mock_provider.get_credentials.return_value = test_value
|
|
2175
|
+
task.provider = mock_provider
|
|
2176
|
+
|
|
2177
|
+
task()
|
|
2178
|
+
|
|
2179
|
+
# Load the file back and verify
|
|
2180
|
+
from dotenv import dotenv_values
|
|
2181
|
+
|
|
2182
|
+
loaded_values = dotenv_values(env_path)
|
|
2183
|
+
assert "ALL_ESCAPES" in loaded_values
|
|
2184
|
+
assert loaded_values["ALL_ESCAPES"] == test_value
|
|
2185
|
+
|
|
2186
|
+
def test_roundtrip_with_unicode_and_escapes(self):
|
|
2187
|
+
"""Test writing and loading .env file with unicode and escape characters."""
|
|
2188
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
2189
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
2190
|
+
|
|
2191
|
+
task_config = TaskConfig(
|
|
2192
|
+
{
|
|
2193
|
+
"options": {
|
|
2194
|
+
"env_path": env_path,
|
|
2195
|
+
"secrets": ["UNICODE_ESCAPE"],
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
)
|
|
2199
|
+
|
|
2200
|
+
project_config = mock.Mock()
|
|
2201
|
+
project_config.repo_root = tmpdir
|
|
2202
|
+
|
|
2203
|
+
task = SecretsToEnv(
|
|
2204
|
+
project_config=project_config,
|
|
2205
|
+
task_config=task_config,
|
|
2206
|
+
org_config=None,
|
|
2207
|
+
)
|
|
2208
|
+
|
|
2209
|
+
mock_provider = mock.Mock()
|
|
2210
|
+
mock_provider.provider_type = "local"
|
|
2211
|
+
unicode_value = 'Hello 世界\n🌍 "Привет"\tДруг'
|
|
2212
|
+
mock_provider.get_credentials.return_value = unicode_value
|
|
2213
|
+
task.provider = mock_provider
|
|
2214
|
+
|
|
2215
|
+
task()
|
|
2216
|
+
|
|
2217
|
+
# Load the file back and verify
|
|
2218
|
+
from dotenv import dotenv_values
|
|
2219
|
+
|
|
2220
|
+
loaded_values = dotenv_values(env_path)
|
|
2221
|
+
assert "UNICODE_ESCAPE" in loaded_values
|
|
2222
|
+
assert loaded_values["UNICODE_ESCAPE"] == unicode_value
|
|
2223
|
+
|
|
2224
|
+
def test_env_file_format_validation(self):
|
|
2225
|
+
"""Test that the .env file format is valid and well-formed."""
|
|
2226
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
2227
|
+
env_path = os.path.join(tmpdir, ".env")
|
|
2228
|
+
|
|
2229
|
+
task_config = TaskConfig(
|
|
2230
|
+
{
|
|
2231
|
+
"options": {
|
|
2232
|
+
"env_path": env_path,
|
|
2233
|
+
"secrets": ["KEY1", "KEY2"],
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
)
|
|
2237
|
+
|
|
2238
|
+
project_config = mock.Mock()
|
|
2239
|
+
project_config.repo_root = tmpdir
|
|
2240
|
+
|
|
2241
|
+
task = SecretsToEnv(
|
|
2242
|
+
project_config=project_config,
|
|
2243
|
+
task_config=task_config,
|
|
2244
|
+
org_config=None,
|
|
2245
|
+
)
|
|
2246
|
+
|
|
2247
|
+
mock_provider = mock.Mock()
|
|
2248
|
+
mock_provider.provider_type = "local"
|
|
2249
|
+
mock_provider.get_credentials.side_effect = ["value1", "value2"]
|
|
2250
|
+
task.provider = mock_provider
|
|
2251
|
+
|
|
2252
|
+
task()
|
|
2253
|
+
|
|
2254
|
+
# Read and validate file format
|
|
2255
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
|
2256
|
+
lines = f.readlines()
|
|
2257
|
+
|
|
2258
|
+
# Each line should be in format: KEY="VALUE"\n
|
|
2259
|
+
assert len(lines) == 2
|
|
2260
|
+
assert lines[0] == 'KEY1="value1"\n'
|
|
2261
|
+
assert lines[1] == 'KEY2="value2"\n'
|
|
2262
|
+
|
|
2263
|
+
# Verify no errors when loading
|
|
2264
|
+
from dotenv import dotenv_values
|
|
2265
|
+
|
|
2266
|
+
loaded_values = dotenv_values(env_path)
|
|
2267
|
+
assert len(loaded_values) == 2
|
|
2268
|
+
assert loaded_values["KEY1"] == "value1"
|
|
2269
|
+
assert loaded_values["KEY2"] == "value2"
|