"""Unit tests for EnvInspectExecutor.""" 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_inspect_executor import EnvInspectExecutor class TestEnvInspectExecutor: """Test suite for EnvInspectExecutor.""" @pytest.fixture def executor(self): """Create executor instance with mocked logger.""" with patch("app.executors.base.get_logger", return_value=MagicMock()): return EnvInspectExecutor() @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 # ==================== Basic Inspection Tests ==================== @pytest.mark.asyncio async def test_inspect_all_keys(self, executor, temp_env_root, mock_settings): """Test reading all keys when no filter is provided.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): env_path = temp_env_root / "app.env" env_path.write_text("KEY1=value1\nKEY2=value2\nKEY3=value3\n") result = await executor.execute({ "path": str(env_path), }) assert result.success is True assert result.data["path"] == str(env_path) assert result.data["keys"] == { "KEY1": "value1", "KEY2": "value2", "KEY3": "value3", } @pytest.mark.asyncio async def test_inspect_selected_keys(self, executor, temp_env_root, mock_settings): """Test reading only selected keys.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): env_path = temp_env_root / "app.env" env_path.write_text("KEY1=value1\nKEY2=value2\nKEY3=value3\n") result = await executor.execute({ "path": str(env_path), "keys": ["KEY1", "KEY3"], }) assert result.success is True assert result.data["keys"] == { "KEY1": "value1", "KEY3": "value3", } @pytest.mark.asyncio async def test_inspect_selected_keys_ignores_unknown(self, executor, temp_env_root, mock_settings): """Test that unknown keys in filter are silently ignored.""" with patch("app.executors.env_inspect_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), "keys": ["KEY1", "NONEXISTENT", "ALSO_MISSING"], }) assert result.success is True assert result.data["keys"] == {"KEY1": "value1"} @pytest.mark.asyncio async def test_inspect_empty_keys_filter(self, executor, temp_env_root, mock_settings): """Test with empty keys filter returns nothing.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): env_path = temp_env_root / "app.env" env_path.write_text("KEY1=value1\n") result = await executor.execute({ "path": str(env_path), "keys": [], }) assert result.success is True assert result.data["keys"] == {} @pytest.mark.asyncio async def test_inspect_with_keys_null(self, executor, temp_env_root, mock_settings): """Test that keys=null returns all keys (same as omitting keys).""" with patch("app.executors.env_inspect_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), "keys": None, }) assert result.success is True assert result.data["keys"] == {"KEY1": "value1", "KEY2": "value2"} # ==================== File Not Found Tests ==================== @pytest.mark.asyncio async def test_missing_file(self, executor, temp_env_root, mock_settings): """Test error when file does not exist.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): env_path = temp_env_root / "nonexistent.env" result = await executor.execute({ "path": str(env_path), }) assert result.success is False assert "does not exist" in result.error or "Path validation failed" in result.error # ==================== Path Validation Tests ==================== @pytest.mark.asyncio async def test_path_traversal_rejected(self, executor, temp_env_root, mock_settings): """Test rejection of directory traversal attempts.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): result = await executor.execute({ "path": str(temp_env_root / ".." / ".." / "etc" / "passwd"), }) assert result.success is False assert "Path validation failed" in result.error or "traversal" in result.error.lower() @pytest.mark.asyncio async def test_path_outside_allowed_root(self, executor, temp_env_root, mock_settings): """Test rejection of paths outside allowed root.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): result = await executor.execute({ "path": "/etc/passwd", }) assert result.success is False assert "Path validation failed" in result.error @pytest.mark.asyncio async def test_path_in_allowed_root_nested(self, executor, temp_env_root, mock_settings): """Test acceptance of valid nested path within allowed root.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): nested_dir = temp_env_root / "subdir" nested_dir.mkdir() env_path = nested_dir / "app.env" env_path.write_text("KEY=value\n") result = await executor.execute({ "path": str(env_path), }) assert result.success is True assert result.data["keys"] == {"KEY": "value"} # ==================== Payload Validation Tests ==================== @pytest.mark.asyncio async def test_missing_path_payload(self, executor): """Test rejection of payload without path.""" with pytest.raises(ValueError, match="Missing required field: path"): await executor.execute({}) @pytest.mark.asyncio async def test_missing_path_payload_with_keys(self, executor): """Test rejection of payload with keys but no path.""" with pytest.raises(ValueError, match="Missing required field: path"): await executor.execute({ "keys": ["KEY1"], }) @pytest.mark.asyncio async def test_reject_invalid_keys_type(self, executor, temp_env_root, mock_settings): """Test rejection when keys is not a list or null.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): env_path = temp_env_root / "app.env" env_path.write_text("KEY=value\n") result = await executor.execute({ "path": str(env_path), "keys": {"not": "a_list"}, }) assert result.success is False assert "'keys' must be a list" in result.error @pytest.mark.asyncio async def test_reject_keys_as_string(self, executor, temp_env_root, mock_settings): """Test rejection when keys is a string instead of list.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): env_path = temp_env_root / "app.env" env_path.write_text("KEY=value\n") result = await executor.execute({ "path": str(env_path), "keys": "KEY", }) assert result.success is False assert "'keys' must be a list" in result.error # ==================== Task Type Tests ==================== def test_task_type_property(self, executor): """Test that task_type returns ENV_INSPECT.""" assert executor.task_type == "ENV_INSPECT" # ==================== Registry Integration Tests ==================== def test_registry_integration(self): """Test that ENV_INSPECT is registered in executor registry.""" from app.executors import get_executor executor = get_executor("ENV_INSPECT") assert executor is not None assert executor.task_type == "ENV_INSPECT" # ==================== Empty File Tests ==================== @pytest.mark.asyncio async def test_empty_file(self, executor, temp_env_root, mock_settings): """Test reading an empty ENV file returns empty keys dict.""" with patch("app.executors.env_inspect_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), }) assert result.success is True assert result.data["keys"] == {} @pytest.mark.asyncio async def test_file_with_only_comments(self, executor, temp_env_root, mock_settings): """Test reading file with only comments returns empty keys dict.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): env_path = temp_env_root / "comments.env" env_path.write_text("# This is a comment\n# Another comment\n") result = await executor.execute({ "path": str(env_path), }) assert result.success is True assert result.data["keys"] == {} # ==================== Parsing Tests ==================== @pytest.mark.asyncio async def test_parses_quoted_values_double(self, executor, temp_env_root, mock_settings): """Test parsing of double-quoted values.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): env_path = temp_env_root / "app.env" env_path.write_text('KEY="value with spaces"\n') result = await executor.execute({ "path": str(env_path), }) assert result.success is True assert result.data["keys"]["KEY"] == "value with spaces" @pytest.mark.asyncio async def test_parses_quoted_values_single(self, executor, temp_env_root, mock_settings): """Test parsing of single-quoted values.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): env_path = temp_env_root / "app.env" env_path.write_text("KEY='single quoted'\n") result = await executor.execute({ "path": str(env_path), }) assert result.success is True assert result.data["keys"]["KEY"] == "single quoted" @pytest.mark.asyncio async def test_parses_value_with_equals(self, executor, temp_env_root, mock_settings): """Test parsing values containing equals signs.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): env_path = temp_env_root / "app.env" env_path.write_text("URL=postgres://user:pass@host/db?opt=val\n") result = await executor.execute({ "path": str(env_path), }) assert result.success is True assert result.data["keys"]["URL"] == "postgres://user:pass@host/db?opt=val" @pytest.mark.asyncio async def test_ignores_comment_lines(self, executor, temp_env_root, mock_settings): """Test that comment lines are ignored.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): env_path = temp_env_root / "app.env" env_path.write_text("# Comment\nKEY=value\n# Another comment\nKEY2=value2\n") result = await executor.execute({ "path": str(env_path), }) assert result.success is True assert result.data["keys"] == {"KEY": "value", "KEY2": "value2"} @pytest.mark.asyncio async def test_ignores_empty_lines(self, executor, temp_env_root, mock_settings): """Test that empty lines are ignored.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): env_path = temp_env_root / "app.env" env_path.write_text("KEY1=value1\n\n\nKEY2=value2\n") result = await executor.execute({ "path": str(env_path), }) assert result.success is True assert result.data["keys"] == {"KEY1": "value1", "KEY2": "value2"} @pytest.mark.asyncio async def test_handles_whitespace_around_key_value(self, executor, temp_env_root, mock_settings): """Test handling of whitespace around keys and values.""" with patch("app.executors.env_inspect_executor.get_settings", return_value=mock_settings): env_path = temp_env_root / "app.env" env_path.write_text(" KEY1 = value1 \n") result = await executor.execute({ "path": str(env_path), }) assert result.success is True assert result.data["keys"]["KEY1"] == "value1" class TestEnvInspectExecutorInternal: """Tests for internal methods of EnvInspectExecutor.""" @pytest.fixture def executor(self): """Create executor instance with mocked logger.""" with patch("app.executors.base.get_logger", return_value=MagicMock()): return EnvInspectExecutor() 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_parse_env_file_empty(self, executor): """Test parsing empty content.""" content = "" result = executor._parse_env_file(content) assert result == {} def test_parse_env_file_only_comments(self, executor): """Test parsing content with only comments.""" content = "# Comment 1\n# Comment 2\n" result = executor._parse_env_file(content) assert result == {}