cumulusci-plus 5.0.43__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/core/config/sfdx_org_config.py +4 -1
- cumulusci/tasks/utility/secretsToEnv.py +40 -9
- cumulusci/tasks/utility/tests/test_secretsToEnv.py +1198 -47
- {cumulusci_plus-5.0.43.dist-info → cumulusci_plus-5.0.45.dist-info}/METADATA +4 -5
- {cumulusci_plus-5.0.43.dist-info → cumulusci_plus-5.0.45.dist-info}/RECORD +11 -11
- {cumulusci_plus-5.0.43.dist-info → cumulusci_plus-5.0.45.dist-info}/WHEEL +0 -0
- {cumulusci_plus-5.0.43.dist-info → cumulusci_plus-5.0.45.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.43.dist-info → cumulusci_plus-5.0.45.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.43.dist-info → cumulusci_plus-5.0.45.dist-info}/licenses/LICENSE +0 -0
|
@@ -220,11 +220,10 @@ class TestSecretsToEnvGetCredential:
|
|
|
220
220
|
mock_provider.get_credentials.return_value = "secret_value_123"
|
|
221
221
|
task.provider = mock_provider
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
original_value = task._get_credential(
|
|
224
224
|
"API_KEY", "api_key", secret_name="my-secret"
|
|
225
225
|
)
|
|
226
226
|
|
|
227
|
-
assert safe_value == "secret_value_123"
|
|
228
227
|
assert original_value == "secret_value_123"
|
|
229
228
|
mock_provider.get_credentials.assert_called_once_with(
|
|
230
229
|
"API_KEY", {"value": "api_key", "secret_name": "my-secret"}
|
|
@@ -247,9 +246,9 @@ class TestSecretsToEnvGetCredential:
|
|
|
247
246
|
mock_provider.get_credentials.return_value = 'value_with_"quotes"'
|
|
248
247
|
task.provider = mock_provider
|
|
249
248
|
|
|
250
|
-
|
|
249
|
+
original_value = task._get_credential("API_KEY", "api_key")
|
|
251
250
|
|
|
252
|
-
assert
|
|
251
|
+
assert original_value == 'value_with_"quotes"'
|
|
253
252
|
|
|
254
253
|
def test_get_credential_escapes_newlines(self):
|
|
255
254
|
"""Test _get_credential escapes newlines."""
|
|
@@ -268,9 +267,9 @@ class TestSecretsToEnvGetCredential:
|
|
|
268
267
|
mock_provider.get_credentials.return_value = "line1\nline2\nline3"
|
|
269
268
|
task.provider = mock_provider
|
|
270
269
|
|
|
271
|
-
|
|
270
|
+
original_value = task._get_credential("API_KEY", "api_key")
|
|
272
271
|
|
|
273
|
-
assert
|
|
272
|
+
assert original_value == "line1\nline2\nline3"
|
|
274
273
|
|
|
275
274
|
def test_get_credential_handles_both_quotes_and_newlines(self):
|
|
276
275
|
"""Test _get_credential handles both quotes and newlines."""
|
|
@@ -289,9 +288,9 @@ class TestSecretsToEnvGetCredential:
|
|
|
289
288
|
mock_provider.get_credentials.return_value = 'line1 "quoted"\nline2'
|
|
290
289
|
task.provider = mock_provider
|
|
291
290
|
|
|
292
|
-
|
|
291
|
+
original_value = task._get_credential("API_KEY", "api_key")
|
|
293
292
|
|
|
294
|
-
assert
|
|
293
|
+
assert original_value == 'line1 "quoted"\nline2'
|
|
295
294
|
|
|
296
295
|
def test_get_credential_with_none_value_raises_error(self):
|
|
297
296
|
"""Test _get_credential raises error when provider returns None."""
|
|
@@ -332,11 +331,11 @@ class TestSecretsToEnvGetCredential:
|
|
|
332
331
|
mock_provider.get_credentials.return_value = "secret_value"
|
|
333
332
|
task.provider = mock_provider
|
|
334
333
|
|
|
335
|
-
|
|
334
|
+
original_value = task._get_credential(
|
|
336
335
|
"CREDENTIAL_KEY", "value", env_key="CUSTOM_ENV_KEY"
|
|
337
336
|
)
|
|
338
337
|
|
|
339
|
-
assert
|
|
338
|
+
assert original_value == "secret_value"
|
|
340
339
|
|
|
341
340
|
def test_get_credential_logs_masked_value(self, caplog):
|
|
342
341
|
"""Test _get_credential logs masked value."""
|
|
@@ -421,7 +420,7 @@ class TestSecretsToEnvGetAllCredentials:
|
|
|
421
420
|
|
|
422
421
|
result = task._get_all_credentials("*")
|
|
423
422
|
|
|
424
|
-
assert result["KEY1"] == 'value_with_
|
|
423
|
+
assert result["KEY1"] == 'value_with_"quotes"'
|
|
425
424
|
assert result["KEY2"] == "normal_value"
|
|
426
425
|
|
|
427
426
|
def test_get_all_credentials_escapes_newlines(self):
|
|
@@ -446,7 +445,7 @@ class TestSecretsToEnvGetAllCredentials:
|
|
|
446
445
|
|
|
447
446
|
result = task._get_all_credentials("*")
|
|
448
447
|
|
|
449
|
-
assert result["KEY1"] == "line1
|
|
448
|
+
assert result["KEY1"] == "line1\nline2"
|
|
450
449
|
assert result["KEY2"] == "single_line"
|
|
451
450
|
|
|
452
451
|
def test_get_all_credentials_with_none_value_raises_error(self):
|
|
@@ -967,23 +966,187 @@ class TestSecretsToEnvIntegration:
|
|
|
967
966
|
assert 'API_TOKEN="ado_token_value"' in content
|
|
968
967
|
|
|
969
968
|
|
|
970
|
-
class
|
|
971
|
-
"""Test
|
|
969
|
+
class TestSecretsToEnvReturnValues:
|
|
970
|
+
"""Test cases for return_values from task execution."""
|
|
972
971
|
|
|
973
|
-
def
|
|
974
|
-
"""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."""
|
|
975
1138
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
976
1139
|
env_path = os.path.join(tmpdir, ".env")
|
|
977
1140
|
|
|
978
1141
|
# Create existing .env file
|
|
979
1142
|
with open(env_path, "w") as f:
|
|
980
|
-
f.write('
|
|
1143
|
+
f.write('EXISTING_KEY="existing_value"\n')
|
|
981
1144
|
|
|
982
1145
|
task_config = TaskConfig(
|
|
983
1146
|
{
|
|
984
1147
|
"options": {
|
|
985
1148
|
"env_path": env_path,
|
|
986
|
-
"secrets": [],
|
|
1149
|
+
"secrets": ["NEW_KEY"],
|
|
987
1150
|
}
|
|
988
1151
|
}
|
|
989
1152
|
)
|
|
@@ -997,20 +1160,25 @@ class TestSecretsToEnvEdgeCases:
|
|
|
997
1160
|
org_config=None,
|
|
998
1161
|
)
|
|
999
1162
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
|
1003
1167
|
|
|
1004
1168
|
task()
|
|
1005
1169
|
|
|
1006
|
-
# Verify existing
|
|
1007
|
-
|
|
1008
|
-
|
|
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"
|
|
1009
1175
|
|
|
1010
|
-
assert 'EXISTING="value"' in content
|
|
1011
1176
|
|
|
1012
|
-
|
|
1013
|
-
|
|
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."""
|
|
1014
1182
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1015
1183
|
env_path = os.path.join(tmpdir, ".env")
|
|
1016
1184
|
|
|
@@ -1018,7 +1186,7 @@ class TestSecretsToEnvEdgeCases:
|
|
|
1018
1186
|
{
|
|
1019
1187
|
"options": {
|
|
1020
1188
|
"env_path": env_path,
|
|
1021
|
-
"secrets": ["
|
|
1189
|
+
"secrets": ["KEY1", "KEY2", "KEY3"],
|
|
1022
1190
|
}
|
|
1023
1191
|
}
|
|
1024
1192
|
)
|
|
@@ -1034,18 +1202,24 @@ class TestSecretsToEnvEdgeCases:
|
|
|
1034
1202
|
|
|
1035
1203
|
mock_provider = mock.Mock()
|
|
1036
1204
|
mock_provider.provider_type = "local"
|
|
1037
|
-
mock_provider.get_credentials.
|
|
1038
|
-
"value!@#$%^&*()[]{}|\\;':<>?,./~`"
|
|
1039
|
-
)
|
|
1205
|
+
mock_provider.get_credentials.side_effect = ["value1", "value2", "value3"]
|
|
1040
1206
|
task.provider = mock_provider
|
|
1041
1207
|
|
|
1042
1208
|
task()
|
|
1043
1209
|
|
|
1044
|
-
#
|
|
1045
|
-
|
|
1210
|
+
# Read raw file content
|
|
1211
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
|
1212
|
+
lines = f.readlines()
|
|
1046
1213
|
|
|
1047
|
-
|
|
1048
|
-
|
|
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."""
|
|
1049
1223
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1050
1224
|
env_path = os.path.join(tmpdir, ".env")
|
|
1051
1225
|
|
|
@@ -1053,7 +1227,7 @@ class TestSecretsToEnvEdgeCases:
|
|
|
1053
1227
|
{
|
|
1054
1228
|
"options": {
|
|
1055
1229
|
"env_path": env_path,
|
|
1056
|
-
"secrets": ["
|
|
1230
|
+
"secrets": ["QUOTE_KEY"],
|
|
1057
1231
|
}
|
|
1058
1232
|
}
|
|
1059
1233
|
)
|
|
@@ -1069,19 +1243,22 @@ class TestSecretsToEnvEdgeCases:
|
|
|
1069
1243
|
|
|
1070
1244
|
mock_provider = mock.Mock()
|
|
1071
1245
|
mock_provider.provider_type = "local"
|
|
1072
|
-
mock_provider.get_credentials.return_value =
|
|
1246
|
+
mock_provider.get_credentials.return_value = 'She said "Hello"'
|
|
1073
1247
|
task.provider = mock_provider
|
|
1074
1248
|
|
|
1075
1249
|
task()
|
|
1076
1250
|
|
|
1077
|
-
#
|
|
1251
|
+
# Read raw file content
|
|
1078
1252
|
with open(env_path, "r", encoding="utf-8") as f:
|
|
1079
1253
|
content = f.read()
|
|
1080
1254
|
|
|
1081
|
-
|
|
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]
|
|
1082
1259
|
|
|
1083
|
-
def
|
|
1084
|
-
"""Test
|
|
1260
|
+
def test_env_file_escapes_newlines_correctly(self):
|
|
1261
|
+
"""Test that newlines in values are escaped correctly in file."""
|
|
1085
1262
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1086
1263
|
env_path = os.path.join(tmpdir, ".env")
|
|
1087
1264
|
|
|
@@ -1089,7 +1266,7 @@ class TestSecretsToEnvEdgeCases:
|
|
|
1089
1266
|
{
|
|
1090
1267
|
"options": {
|
|
1091
1268
|
"env_path": env_path,
|
|
1092
|
-
"secrets": ["
|
|
1269
|
+
"secrets": ["MULTILINE_KEY"],
|
|
1093
1270
|
}
|
|
1094
1271
|
}
|
|
1095
1272
|
)
|
|
@@ -1105,14 +1282,988 @@ class TestSecretsToEnvEdgeCases:
|
|
|
1105
1282
|
|
|
1106
1283
|
mock_provider = mock.Mock()
|
|
1107
1284
|
mock_provider.provider_type = "local"
|
|
1108
|
-
|
|
1109
|
-
mock_provider.get_credentials.return_value = long_value
|
|
1285
|
+
mock_provider.get_credentials.return_value = "line1\nline2\nline3"
|
|
1110
1286
|
task.provider = mock_provider
|
|
1111
1287
|
|
|
1112
1288
|
task()
|
|
1113
1289
|
|
|
1114
|
-
#
|
|
1115
|
-
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:
|
|
1116
1332
|
content = f.read()
|
|
1117
1333
|
|
|
1118
|
-
|
|
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"
|