Skip to main content
Glama
test_executor.py13.5 kB
import tempfile from pathlib import Path import pytest from hooks_mcp.config import Action, ActionParameter, ParameterType from hooks_mcp.executor import CommandExecutor, ExecutionError class TestExecutor: """Test command execution functionality.""" @pytest.fixture(autouse=True) def setup_teardown(self): """Set up and tear down test environment.""" # Create a temporary directory for testing self.temp_dir = tempfile.TemporaryDirectory() self.project_root = Path(self.temp_dir.name) # Create some test files and directories (self.project_root / "tests").mkdir() (self.project_root / "tests" / "test1.py").touch() (self.project_root / "src").mkdir() (self.project_root / "src" / "main.py").touch() self.executor = CommandExecutor() self.executor.project_root = self.project_root # This will run after each test yield # Clean up test environment self.temp_dir.cleanup() def test_execute_simple_command(self): """Test executing a simple command without parameters.""" action = Action( name="echo_test", description="Echo test", command="echo Hello World" ) result = self.executor.execute_action(action, {}) assert result["status_code"] == 0 assert "Hello World" in result["stdout"] assert result["stderr"] == "" def test_execute_command_with_project_file_path(self): """Test executing a command with a project file path parameter.""" action = Action( name="cat_test", description="Cat test file", command="cat $TEST_FILE", parameters=[ ActionParameter( "TEST_FILE", ParameterType.PROJECT_FILE_PATH, "Test file to cat" ) ], ) # This should work with a valid path result = self.executor.execute_action(action, {"TEST_FILE": "tests/test1.py"}) assert result["status_code"] == 0 assert result["stdout"] == "" assert result["stderr"] == "" def test_execute_command_with_insecure_string(self): """Test executing a command with an insecure string parameter.""" action = Action( name="echo_insecure", description="Echo insecure string", command="echo $MESSAGE", parameters=[ ActionParameter( "MESSAGE", ParameterType.INSECURE_STRING, "Message to echo" ) ], ) result = self.executor.execute_action(action, {"MESSAGE": "Hello World"}) assert result["status_code"] == 0 assert "Hello World" in result["stdout"] assert result["stderr"] == "" def test_execute_command_with_default_parameters(self): """Test executing a command with default parameter values.""" action = Action( name="echo_default", description="Echo with default", command="echo $MESSAGE", parameters=[ ActionParameter( "MESSAGE", ParameterType.INSECURE_STRING, "Message to echo", "Default Message", ) ], ) # Execute without providing the parameter, should use default result = self.executor.execute_action(action, {}) assert result["status_code"] == 0 assert "Default Message" in result["stdout"] assert result["stderr"] == "" def test_execute_command_missing_required_parameter(self): """Test that executing a command without a required parameter raises an error.""" action = Action( name="echo_required", description="Echo with required parameter", command="echo $MESSAGE", parameters=[ ActionParameter( "MESSAGE", ParameterType.INSECURE_STRING, "Message to echo" ) ], ) with pytest.raises(ExecutionError) as context: self.executor.execute_action(action, {}) assert "Required parameter 'MESSAGE' not provided" in str(context.value) def test_execute_command_with_run_path(self): """Test executing a command with a run_path.""" # Create a subdirectory for testing (self.project_root / "subdir").mkdir() (self.project_root / "subdir" / "file.txt").touch() action = Action( name="pwd_test", description="Print working directory", command="pwd", run_path="subdir", ) result = self.executor.execute_action(action, {}) assert result["status_code"] == 0 # The output should contain the subdir path assert "subdir" in result["stdout"] def test_execute_command_invalid_run_path(self): """Test that executing a command with an invalid run_path raises an error.""" action = Action( name="echo_test", description="Echo test", command="echo Hello World", run_path="../outside", ) with pytest.raises(ExecutionError) as context: self.executor.execute_action(action, {}) assert "Invalid run_path" in str(context.value) def test_execute_command_with_insecure_string_prevents_command_injection(self): """Test that insecure string parameters don't allow command injection.""" action = Action( name="echo_insecure", description="Echo insecure string", command="echo $MESSAGE", parameters=[ ActionParameter( "MESSAGE", ParameterType.INSECURE_STRING, "Message to echo" ) ], ) # This test checks that the command doesn't execute the second part of the string as a separate command # If vulnerable to injection, this would execute both "echo 123" and "echo 456" result = self.executor.execute_action(action, {"MESSAGE": "123 && echo 456"}) assert result["status_code"] == 0 # The output should contain the full string "123 && echo 456" and not just "123" # It should not contain "456" on a separate line assert "123 && echo 456" in result["stdout"] assert "456" not in result["stdout"].split("\n") def test_execute_command_with_insecure_string_prevents_semicolon_injection(self): """Test that insecure string parameters don't allow semicolon command injection.""" action = Action( name="echo_insecure", description="Echo insecure string", command="echo $MESSAGE", parameters=[ ActionParameter( "MESSAGE", ParameterType.INSECURE_STRING, "Message to echo" ) ], ) # Test semicolon command injection result = self.executor.execute_action(action, {"MESSAGE": "123; echo 456"}) assert result["status_code"] == 0 # The output should contain the full string "123; echo 456" and not just "123" # It should not contain "456" on a separate line assert "123; echo 456" in result["stdout"] assert "456" not in result["stdout"].split("\n") def test_execute_command_with_insecure_string_prevents_pipe_injection(self): """Test that insecure string parameters don't allow pipe command injection.""" action = Action( name="echo_insecure", description="Echo insecure string", command="echo $MESSAGE", parameters=[ ActionParameter( "MESSAGE", ParameterType.INSECURE_STRING, "Message to echo" ) ], ) # Test pipe command injection result = self.executor.execute_action(action, {"MESSAGE": "123 | echo 456"}) assert result["status_code"] == 0 # The output should contain the full string "123 | echo 456" assert "123 | echo 456" in result["stdout"] def test_execute_command_with_insecure_string_prevents_redirect_injection(self): """Test that insecure string parameters don't allow redirect command injection.""" action = Action( name="echo_insecure", description="Echo insecure string", command="echo $MESSAGE", parameters=[ ActionParameter( "MESSAGE", ParameterType.INSECURE_STRING, "Message to echo" ) ], ) # Test redirect command injection result = self.executor.execute_action(action, {"MESSAGE": "123 > test.txt"}) assert result["status_code"] == 0 # The output should contain the full string "123 > test.txt" assert "123 > test.txt" in result["stdout"] def test_execute_command_with_insecure_string_prevents_subshell_injection(self): """Test that insecure string parameters don't allow subshell command injection.""" action = Action( name="echo_insecure", description="Echo insecure string", command="echo $MESSAGE", parameters=[ ActionParameter( "MESSAGE", ParameterType.INSECURE_STRING, "Message to echo" ) ], ) # Test subshell command injection result = self.executor.execute_action(action, {"MESSAGE": "123 $(echo 456)"}) assert result["status_code"] == 0 # The output should contain the full string "123 $(echo 456)" assert "123 $(echo 456)" in result["stdout"] def test_execute_command_with_insecure_string_prevents_backtick_injection(self): """Test that insecure string parameters don't allow backtick command injection.""" action = Action( name="echo_insecure", description="Echo insecure string", command="echo $MESSAGE", parameters=[ ActionParameter( "MESSAGE", ParameterType.INSECURE_STRING, "Message to echo" ) ], ) # Test backtick command injection result = self.executor.execute_action(action, {"MESSAGE": "123 `echo 456`"}) assert result["status_code"] == 0 # The output should contain the full string "123 `echo 456`" assert "123 `echo 456`" in result["stdout"] def test_execute_command_with_required_env_var_substitution(self): """Test that required_env_var parameters get substituted in commands. This test demonstrates a bug: env vars are not substituted because the substitution line is commented out and env vars are skipped in the loop. """ import os # Set up environment variable original_value = os.environ.get("TEST_MESSAGE") os.environ["TEST_MESSAGE"] = "Hello from env var" try: action = Action( name="echo_env_var", description="Echo environment variable", command="echo $TEST_MESSAGE", parameters=[ ActionParameter( "TEST_MESSAGE", ParameterType.REQUIRED_ENV_VAR, "Test message from environment", ) ], ) result = self.executor.execute_action(action, {}) assert result["status_code"] == 0 # This should pass if env var substitution works, but will fail with current code assert "Hello from env var" in result["stdout"] # This assertion shows what actually happens - literal $TEST_MESSAGE assert "$TEST_MESSAGE" not in result["stdout"] finally: # Clean up environment if original_value is None: os.environ.pop("TEST_MESSAGE", None) else: os.environ["TEST_MESSAGE"] = original_value def test_execute_command_with_timeout(self): """Test that the timeout parameter is respected.""" # Create an action with a short timeout (1 second) action = Action( name="sleep_test", description="Sleep test", command="sleep 5", # This will take longer than the timeout timeout=1, # 1 second timeout ) # Execute the action and expect a timeout error with pytest.raises(ExecutionError) as context: self.executor.execute_action(action, {}) # Verify the error message contains the correct timeout value assert "timed out after 1 seconds" in str(context.value) def test_execute_command_with_default_timeout(self): """Test that the default timeout is used when not specified.""" # Create an action without specifying timeout (should default to 60 seconds) action = Action( name="sleep_test_default", description="Sleep test with default timeout", command="sleep 1", # This will complete within the default timeout ) # Execute the action - it should complete successfully result = self.executor.execute_action(action, {}) assert result["status_code"] == 0 assert result["stderr"] == ""

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/scosman/actions_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server