583 lines
24 KiB
Python
583 lines
24 KiB
Python
"""Unit tests for EnvUpdateExecutor."""
|
|
|
|
import os
|
|
import stat
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
# Patch the logger before importing the executor
|
|
with patch("app.utils.logger.get_logger", return_value=MagicMock()):
|
|
from app.executors.env_update_executor import EnvUpdateExecutor
|
|
|
|
|
|
class TestEnvUpdateExecutor:
|
|
"""Test suite for EnvUpdateExecutor."""
|
|
|
|
@pytest.fixture
|
|
def executor(self):
|
|
"""Create executor instance with mocked logger."""
|
|
with patch("app.executors.base.get_logger", return_value=MagicMock()):
|
|
return EnvUpdateExecutor()
|
|
|
|
@pytest.fixture
|
|
def temp_env_root(self, tmp_path):
|
|
"""Create a temporary directory to act as /opt/letsbe/env."""
|
|
env_dir = tmp_path / "opt" / "letsbe" / "env"
|
|
env_dir.mkdir(parents=True)
|
|
return env_dir
|
|
|
|
@pytest.fixture
|
|
def mock_settings(self, temp_env_root):
|
|
"""Mock settings with temporary env root."""
|
|
settings = MagicMock()
|
|
settings.allowed_env_root = str(temp_env_root)
|
|
return settings
|
|
|
|
# ==================== New File Creation Tests ====================
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_new_env_file(self, executor, temp_env_root, mock_settings):
|
|
"""Test creating a new ENV file when it doesn't exist."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "newapp.env"
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {
|
|
"DATABASE_URL": "postgres://localhost/mydb",
|
|
"API_KEY": "secret123",
|
|
},
|
|
})
|
|
|
|
assert result.success is True
|
|
assert set(result.data["updated_keys"]) == {"DATABASE_URL", "API_KEY"}
|
|
assert result.data["removed_keys"] == []
|
|
assert result.data["path"] == str(env_path)
|
|
|
|
# Verify file was created
|
|
assert env_path.exists()
|
|
content = env_path.read_text()
|
|
assert "API_KEY=secret123" in content
|
|
assert "DATABASE_URL=postgres://localhost/mydb" in content
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_env_file_in_nested_directory(self, executor, temp_env_root, mock_settings):
|
|
"""Test creating ENV file in a nested directory that doesn't exist."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "subdir" / "app.env"
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {"KEY": "value"},
|
|
})
|
|
|
|
assert result.success is True
|
|
assert env_path.exists()
|
|
|
|
# ==================== Update Existing Keys Tests ====================
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_existing_keys(self, executor, temp_env_root, mock_settings):
|
|
"""Test updating existing keys in an ENV file."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
env_path.write_text("EXISTING_KEY=old_value\nANOTHER_KEY=keep_this\n")
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {"EXISTING_KEY": "new_value"},
|
|
})
|
|
|
|
assert result.success is True
|
|
assert "EXISTING_KEY" in result.data["updated_keys"]
|
|
|
|
content = env_path.read_text()
|
|
assert "EXISTING_KEY=new_value" in content
|
|
assert "ANOTHER_KEY=keep_this" in content
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_new_keys_to_existing_file(self, executor, temp_env_root, mock_settings):
|
|
"""Test adding new keys to an existing ENV file."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
env_path.write_text("EXISTING_KEY=value\n")
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {"NEW_KEY": "new_value"},
|
|
})
|
|
|
|
assert result.success is True
|
|
content = env_path.read_text()
|
|
assert "EXISTING_KEY=value" in content
|
|
assert "NEW_KEY=new_value" in content
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_preserves_key_values(self, executor, temp_env_root, mock_settings):
|
|
"""Test that existing key values are preserved when not updated."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
env_path.write_text("KEY1=value1\nKEY2=value2\n")
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {"KEY1": "updated"},
|
|
})
|
|
|
|
assert result.success is True
|
|
content = env_path.read_text()
|
|
assert "KEY1=updated" in content
|
|
assert "KEY2=value2" in content
|
|
|
|
# ==================== Remove Keys Tests ====================
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_remove_existing_keys(self, executor, temp_env_root, mock_settings):
|
|
"""Test removing existing keys from an ENV file."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
env_path.write_text("KEEP_KEY=value\nREMOVE_KEY=to_remove\nANOTHER_KEEP=keep\n")
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"remove_keys": ["REMOVE_KEY"],
|
|
})
|
|
|
|
assert result.success is True
|
|
assert result.data["removed_keys"] == ["REMOVE_KEY"]
|
|
assert result.data["updated_keys"] == []
|
|
|
|
content = env_path.read_text()
|
|
assert "REMOVE_KEY" not in content
|
|
assert "KEEP_KEY=value" in content
|
|
assert "ANOTHER_KEEP=keep" in content
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_remove_nonexistent_key(self, executor, temp_env_root, mock_settings):
|
|
"""Test removing a key that doesn't exist (should succeed but not report as removed)."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
env_path.write_text("EXISTING_KEY=value\n")
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"remove_keys": ["NONEXISTENT_KEY"],
|
|
})
|
|
|
|
assert result.success is True
|
|
assert result.data["removed_keys"] == [] # Not reported as removed since it didn't exist
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_and_remove_together(self, executor, temp_env_root, mock_settings):
|
|
"""Test updating and removing keys in the same operation."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
env_path.write_text("KEY1=old\nKEY2=remove_me\nKEY3=keep\n")
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {"KEY1": "new", "NEW_KEY": "added"},
|
|
"remove_keys": ["KEY2"],
|
|
})
|
|
|
|
assert result.success is True
|
|
assert "KEY1" in result.data["updated_keys"]
|
|
assert "NEW_KEY" in result.data["updated_keys"]
|
|
assert result.data["removed_keys"] == ["KEY2"]
|
|
|
|
content = env_path.read_text()
|
|
assert "KEY1=new" in content
|
|
assert "NEW_KEY=added" in content
|
|
assert "KEY3=keep" in content
|
|
assert "KEY2" not in content
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_remove_multiple_keys(self, executor, temp_env_root, mock_settings):
|
|
"""Test removing multiple keys at once."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
env_path.write_text("A=1\nB=2\nC=3\nD=4\n")
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"remove_keys": ["A", "C"],
|
|
})
|
|
|
|
assert result.success is True
|
|
assert set(result.data["removed_keys"]) == {"A", "C"}
|
|
|
|
content = env_path.read_text()
|
|
assert "A=" not in content
|
|
assert "C=" not in content
|
|
assert "B=2" in content
|
|
assert "D=4" in content
|
|
|
|
# ==================== Invalid Key Name Tests ====================
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reject_invalid_update_key_lowercase(self, executor, temp_env_root, mock_settings):
|
|
"""Test rejection of lowercase keys in updates."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {"invalid_key": "value"},
|
|
})
|
|
|
|
assert result.success is False
|
|
assert "Invalid ENV key format" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reject_invalid_update_key_starts_with_number(self, executor, temp_env_root, mock_settings):
|
|
"""Test rejection of keys starting with a number."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {"1INVALID": "value"},
|
|
})
|
|
|
|
assert result.success is False
|
|
assert "Invalid ENV key format" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reject_invalid_update_key_special_chars(self, executor, temp_env_root, mock_settings):
|
|
"""Test rejection of keys with special characters."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {"INVALID-KEY": "value"},
|
|
})
|
|
|
|
assert result.success is False
|
|
assert "Invalid ENV key format" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reject_invalid_remove_key(self, executor, temp_env_root, mock_settings):
|
|
"""Test rejection of invalid keys in remove_keys."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
env_path.write_text("VALID_KEY=value\n")
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"remove_keys": ["invalid_lowercase"],
|
|
})
|
|
|
|
assert result.success is False
|
|
assert "Invalid ENV key format" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_accept_valid_key_formats(self, executor, temp_env_root, mock_settings):
|
|
"""Test acceptance of various valid key formats."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
|
|
valid_keys = {
|
|
"A": "1",
|
|
"AB": "2",
|
|
"A1": "3",
|
|
"A_B": "4",
|
|
"ABC123_XYZ": "5",
|
|
"DATABASE_URL": "postgres://localhost/db",
|
|
}
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": valid_keys,
|
|
})
|
|
|
|
assert result.success is True
|
|
assert set(result.data["updated_keys"]) == set(valid_keys.keys())
|
|
|
|
# ==================== Path Validation Tests ====================
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reject_path_outside_allowed_root(self, executor, temp_env_root, mock_settings):
|
|
"""Test rejection of paths outside /opt/letsbe/env."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
# Try to write to parent directory
|
|
result = await executor.execute({
|
|
"path": "/etc/passwd",
|
|
"updates": {"HACK": "attempt"},
|
|
})
|
|
|
|
assert result.success is False
|
|
assert "Path validation failed" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reject_path_traversal_attack(self, executor, temp_env_root, mock_settings):
|
|
"""Test rejection of directory traversal attempts."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
result = await executor.execute({
|
|
"path": str(temp_env_root / ".." / ".." / "etc" / "passwd"),
|
|
"updates": {"HACK": "attempt"},
|
|
})
|
|
|
|
assert result.success is False
|
|
assert "Path validation failed" in result.error or "traversal" in result.error.lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_accept_valid_path_in_allowed_root(self, executor, temp_env_root, mock_settings):
|
|
"""Test acceptance of valid paths within allowed root."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "valid" / "path" / "app.env"
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {"VALID": "path"},
|
|
})
|
|
|
|
assert result.success is True
|
|
|
|
# ==================== Payload Validation Tests ====================
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reject_missing_path(self, executor):
|
|
"""Test rejection of payload without path."""
|
|
with pytest.raises(ValueError, match="Missing required field: path"):
|
|
await executor.execute({
|
|
"updates": {"KEY": "value"},
|
|
})
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reject_empty_operations(self, executor, temp_env_root, mock_settings):
|
|
"""Test rejection when neither updates nor remove_keys provided."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
result = await executor.execute({
|
|
"path": str(temp_env_root / "app.env"),
|
|
})
|
|
|
|
assert result.success is False
|
|
assert "At least one of 'updates' or 'remove_keys'" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reject_invalid_updates_type(self, executor, temp_env_root, mock_settings):
|
|
"""Test rejection when updates is not a dict."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
result = await executor.execute({
|
|
"path": str(temp_env_root / "app.env"),
|
|
"updates": ["not", "a", "dict"],
|
|
})
|
|
|
|
assert result.success is False
|
|
assert "'updates' must be a dictionary" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reject_invalid_remove_keys_type(self, executor, temp_env_root, mock_settings):
|
|
"""Test rejection when remove_keys is not a list."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
result = await executor.execute({
|
|
"path": str(temp_env_root / "app.env"),
|
|
"remove_keys": {"not": "a_list"},
|
|
})
|
|
|
|
assert result.success is False
|
|
assert "'remove_keys' must be a list" in result.error
|
|
|
|
# ==================== File Permission Tests ====================
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skipif(os.name == "nt", reason="chmod not fully supported on Windows")
|
|
async def test_file_permissions_640(self, executor, temp_env_root, mock_settings):
|
|
"""Test that created files have 640 permissions."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "secure.env"
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {"SECRET": "value"},
|
|
})
|
|
|
|
assert result.success is True
|
|
|
|
# Check file permissions
|
|
file_stat = env_path.stat()
|
|
# 0o640 = owner rw, group r, others none
|
|
expected_mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP
|
|
actual_mode = stat.S_IMODE(file_stat.st_mode)
|
|
assert actual_mode == expected_mode, f"Expected {oct(expected_mode)}, got {oct(actual_mode)}"
|
|
|
|
# ==================== ENV File Parsing Tests ====================
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_parse_quoted_values(self, executor, temp_env_root, mock_settings):
|
|
"""Test parsing of quoted values in ENV files."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
env_path.write_text('QUOTED="value with spaces"\nSINGLE=\'single quoted\'\n')
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {"NEW": "added"},
|
|
})
|
|
|
|
assert result.success is True
|
|
content = env_path.read_text()
|
|
# Values should be preserved (without extra quotes in the parsed form)
|
|
assert "NEW=added" in content
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_values_with_equals(self, executor, temp_env_root, mock_settings):
|
|
"""Test handling of values containing equals signs."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {"URL": "postgres://user:pass@host/db?opt=val"},
|
|
})
|
|
|
|
assert result.success is True
|
|
content = env_path.read_text()
|
|
# Values with = should be quoted
|
|
assert 'URL="postgres://user:pass@host/db?opt=val"' in content
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_keys_sorted_in_output(self, executor, temp_env_root, mock_settings):
|
|
"""Test that keys are sorted alphabetically in output."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {
|
|
"ZEBRA": "last",
|
|
"APPLE": "first",
|
|
"MANGO": "middle",
|
|
},
|
|
})
|
|
|
|
assert result.success is True
|
|
content = env_path.read_text()
|
|
lines = [l for l in content.splitlines() if l]
|
|
keys = [l.split("=")[0] for l in lines]
|
|
assert keys == sorted(keys)
|
|
|
|
# ==================== Edge Cases ====================
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_empty_env_file(self, executor, temp_env_root, mock_settings):
|
|
"""Test handling of empty existing ENV file."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "empty.env"
|
|
env_path.write_text("")
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {"NEW_KEY": "value"},
|
|
})
|
|
|
|
assert result.success is True
|
|
content = env_path.read_text()
|
|
assert "NEW_KEY=value" in content
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_remove_all_keys(self, executor, temp_env_root, mock_settings):
|
|
"""Test removing all keys results in empty file."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
env_path.write_text("ONLY_KEY=value\n")
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"remove_keys": ["ONLY_KEY"],
|
|
})
|
|
|
|
assert result.success is True
|
|
content = env_path.read_text()
|
|
assert content == ""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_value_with_newline(self, executor, temp_env_root, mock_settings):
|
|
"""Test handling values with newlines (should be quoted)."""
|
|
with patch("app.executors.env_update_executor.get_settings", return_value=mock_settings):
|
|
env_path = temp_env_root / "app.env"
|
|
|
|
result = await executor.execute({
|
|
"path": str(env_path),
|
|
"updates": {"MULTILINE": "line1\nline2"},
|
|
})
|
|
|
|
assert result.success is True
|
|
content = env_path.read_text()
|
|
assert 'MULTILINE="line1\nline2"' in content
|
|
|
|
|
|
class TestEnvUpdateExecutorInternal:
|
|
"""Tests for internal methods of EnvUpdateExecutor."""
|
|
|
|
@pytest.fixture
|
|
def executor(self):
|
|
"""Create executor instance with mocked logger."""
|
|
with patch("app.executors.base.get_logger", return_value=MagicMock()):
|
|
return EnvUpdateExecutor()
|
|
|
|
def test_parse_env_file_basic(self, executor):
|
|
"""Test basic ENV file parsing."""
|
|
content = "KEY1=value1\nKEY2=value2\n"
|
|
result = executor._parse_env_file(content)
|
|
assert result == {"KEY1": "value1", "KEY2": "value2"}
|
|
|
|
def test_parse_env_file_with_comments(self, executor):
|
|
"""Test parsing ignores comments."""
|
|
content = "# Comment\nKEY=value\n# Another comment\n"
|
|
result = executor._parse_env_file(content)
|
|
assert result == {"KEY": "value"}
|
|
|
|
def test_parse_env_file_with_empty_lines(self, executor):
|
|
"""Test parsing ignores empty lines."""
|
|
content = "KEY1=value1\n\n\nKEY2=value2\n"
|
|
result = executor._parse_env_file(content)
|
|
assert result == {"KEY1": "value1", "KEY2": "value2"}
|
|
|
|
def test_parse_env_file_with_quotes(self, executor):
|
|
"""Test parsing handles quoted values."""
|
|
content = 'KEY1="quoted value"\nKEY2=\'single quoted\'\n'
|
|
result = executor._parse_env_file(content)
|
|
assert result == {"KEY1": "quoted value", "KEY2": "single quoted"}
|
|
|
|
def test_parse_env_file_with_equals_in_value(self, executor):
|
|
"""Test parsing handles equals signs in values."""
|
|
content = "URL=postgres://user:pass@host/db?opt=val\n"
|
|
result = executor._parse_env_file(content)
|
|
assert result == {"URL": "postgres://user:pass@host/db?opt=val"}
|
|
|
|
def test_serialize_env_basic(self, executor):
|
|
"""Test basic ENV serialization."""
|
|
env_dict = {"KEY1": "value1", "KEY2": "value2"}
|
|
result = executor._serialize_env(env_dict)
|
|
assert "KEY1=value1" in result
|
|
assert "KEY2=value2" in result
|
|
|
|
def test_serialize_env_sorted(self, executor):
|
|
"""Test serialization produces sorted output."""
|
|
env_dict = {"ZEBRA": "z", "APPLE": "a"}
|
|
result = executor._serialize_env(env_dict)
|
|
lines = result.strip().split("\n")
|
|
assert lines[0].startswith("APPLE=")
|
|
assert lines[1].startswith("ZEBRA=")
|
|
|
|
def test_serialize_env_quotes_special_values(self, executor):
|
|
"""Test serialization quotes values with special characters."""
|
|
env_dict = {
|
|
"SPACES": "has spaces",
|
|
"EQUALS": "has=equals",
|
|
"NEWLINE": "has\nnewline",
|
|
}
|
|
result = executor._serialize_env(env_dict)
|
|
assert 'SPACES="has spaces"' in result
|
|
assert 'EQUALS="has=equals"' in result
|
|
assert 'NEWLINE="has\nnewline"' in result
|
|
|
|
def test_serialize_env_empty_dict(self, executor):
|
|
"""Test serialization of empty dict."""
|
|
result = executor._serialize_env({})
|
|
assert result == ""
|