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.
@@ -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
- safe_value, original_value = task._get_credential(
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
- safe_value, _ = task._get_credential("API_KEY", "api_key")
249
+ original_value = task._get_credential("API_KEY", "api_key")
251
250
 
252
- assert safe_value == 'value_with_\\"quotes\\"'
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
- safe_value, _ = task._get_credential("API_KEY", "api_key")
270
+ original_value = task._get_credential("API_KEY", "api_key")
272
271
 
273
- assert safe_value == "line1\\nline2\\nline3"
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
- safe_value, _ = task._get_credential("API_KEY", "api_key")
291
+ original_value = task._get_credential("API_KEY", "api_key")
293
292
 
294
- assert safe_value == 'line1 \\"quoted\\"\\nline2'
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
- safe_value, _ = task._get_credential(
334
+ original_value = task._get_credential(
336
335
  "CREDENTIAL_KEY", "value", env_key="CUSTOM_ENV_KEY"
337
336
  )
338
337
 
339
- assert safe_value == "secret_value"
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_\\"quotes\\"'
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\\nline2"
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 TestSecretsToEnvEdgeCases:
971
- """Test edge cases and error conditions."""
969
+ class TestSecretsToEnvReturnValues:
970
+ """Test cases for return_values from task execution."""
972
971
 
973
- def test_empty_secrets_list_creates_empty_env_file(self):
974
- """Test that empty secrets list still creates/updates env file."""
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('EXISTING="value"\n')
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
- # When secrets is an empty list, _init_secrets doesn't set task.secrets
1001
- # so we need to set it manually for this test
1002
- task.secrets = {}
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 content is preserved
1007
- with open(env_path, "r") as f:
1008
- content = f.read()
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
- def test_special_characters_in_secret_values(self):
1013
- """Test handling of special characters in secret values."""
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": ["SPECIAL"],
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.return_value = (
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
- # Verify file can be read
1045
- assert os.path.exists(env_path)
1210
+ # Read raw file content
1211
+ with open(env_path, "r", encoding="utf-8") as f:
1212
+ lines = f.readlines()
1046
1213
 
1047
- def test_unicode_characters_in_secret_values(self):
1048
- """Test handling of unicode characters in secret values."""
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": ["UNICODE"],
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 = "Hello 世界 🌍 Привет"
1246
+ mock_provider.get_credentials.return_value = 'She said "Hello"'
1073
1247
  task.provider = mock_provider
1074
1248
 
1075
1249
  task()
1076
1250
 
1077
- # Verify file contents
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
- assert 'UNICODE="Hello 世界 🌍 Привет"' in content
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 test_very_long_secret_value(self):
1084
- """Test handling of very long secret values."""
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": ["LONG_SECRET"],
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
- long_value = "x" * 10000 # 10k characters
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
- # Verify file contents
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
- assert f'LONG_SECRET="{long_value}"' in content
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"