""" Tests for Bearer P0 security alert fixes (PR #1934). Verifies: - Shell injection fix: run_command(shell=True) removed from pre_prompt.py - GPU detection uses list-based subprocess.run (no shell=True) - open_file_location uses PathValidator for path validation """ import importlib.util import subprocess import sys from pathlib import Path from unittest.mock import MagicMock, patch def _load_pre_prompt_module(): """Load the pre_prompt module from the cookiecutter-docker hooks directory.""" module_path = ( Path(__file__).resolve().parents[2] / "cookiecutter-docker" / "hooks" / "pre_prompt.py" ) spec = importlib.util.spec_from_file_location( "pre_prompt", str(module_path) ) module = importlib.util.module_from_spec(spec) # Mock cookiecutter.prompt since it may not be installed in test env sys.modules.setdefault("cookiecutter", MagicMock()) sys.modules.setdefault("cookiecutter.prompt", MagicMock()) spec.loader.exec_module(module) return module class TestRunCommandRemoved: """Verify that the insecure run_command function has been removed.""" def test_run_command_not_defined_in_module(self): """run_command(shell=True) must not exist in pre_prompt.py.""" module = _load_pre_prompt_module() import inspect module_functions = { name for name, obj in inspect.getmembers(module, inspect.isfunction) if obj.__module__ == module.__name__ } assert "run_command" not in module_functions, ( "run_command should be removed from pre_prompt.py " "to fix shell injection" ) def test_no_shell_true_in_source(self): """The pre_prompt.py source must not contain shell=True.""" module_path = ( Path(__file__).resolve().parents[2] / "cookiecutter-docker" / "hooks" / "pre_prompt.py" ) source = module_path.read_text() assert "shell=True" not in source, ( "pre_prompt.py must not use shell=True in any subprocess call" ) class TestCheckGpuLinux: """Verify check_gpu_linux uses secure subprocess invocation.""" def test_calls_subprocess_run_with_list_args(self): """check_gpu_linux must call subprocess.run with a list, not a string.""" module = _load_pre_prompt_module() context = {} with patch.object(subprocess, "run") as mock_run: mock_run.return_value = MagicMock( stdout=( "00:02.0 VGA compatible controller: NVIDIA Corporation" ), returncode=0, ) module.check_gpu_linux(context) mock_run.assert_called_once() call_args = mock_run.call_args cmd_arg = ( call_args[0][0] if call_args[0] else call_args[1].get("args") ) assert isinstance(cmd_arg, list), ( f"subprocess.run must be called with a list, " f"got {type(cmd_arg)}" ) assert cmd_arg == ["lspci"] def test_does_not_use_shell_true(self): """check_gpu_linux must not pass shell=True.""" module = _load_pre_prompt_module() context = {} with patch.object(subprocess, "run") as mock_run: mock_run.return_value = MagicMock(stdout="", returncode=0) module.check_gpu_linux(context) call_kwargs = mock_run.call_args[1] if mock_run.call_args[1] else {} assert call_kwargs.get("shell") is not True, ( "check_gpu_linux must not use shell=True" ) def test_detects_nvidia_gpu(self): """check_gpu_linux correctly detects NVIDIA GPU from lspci output.""" module = _load_pre_prompt_module() context = {} nvidia_output = ( "00:02.0 VGA compatible controller: " "NVIDIA Corporation GeForce RTX 3080" ) with patch.object(subprocess, "run") as mock_run: mock_run.return_value = MagicMock( stdout=nvidia_output, returncode=0, ) module.check_gpu_linux(context) assert context["_nvidia_gpu"] is True assert context["_amd_gpu"] is False assert context["enable_gpu"] is True def test_detects_amd_gpu(self): """check_gpu_linux correctly detects AMD GPU from lspci output.""" module = _load_pre_prompt_module() context = {} amd_output = "06:00.0 VGA compatible controller: AMD/ATI Radeon RX 7900" with patch.object(subprocess, "run") as mock_run: mock_run.return_value = MagicMock( stdout=amd_output, returncode=0, ) module.check_gpu_linux(context) assert context["_amd_gpu"] is True assert context["_nvidia_gpu"] is False assert context["enable_gpu"] is True def test_no_gpu_detected(self): """check_gpu_linux handles no GPU detection.""" module = _load_pre_prompt_module() context = {} intel_output = ( "00:02.0 VGA compatible controller: Intel Corporation UHD Graphics" ) with patch.object(subprocess, "run") as mock_run: mock_run.return_value = MagicMock( stdout=intel_output, returncode=0, ) module.check_gpu_linux(context) assert context["_nvidia_gpu"] is False assert context["_amd_gpu"] is False assert context["enable_gpu"] is False def test_filters_vga_lines_in_python(self): """check_gpu_linux filters VGA lines in Python, not via shell pipe.""" module = _load_pre_prompt_module() context = {} lines = [ "00:00.0 Host bridge: Intel Corporation", "00:02.0 VGA compatible controller: NVIDIA Corporation", "00:1f.0 ISA bridge: Intel Corporation", ] lspci_output = chr(10).join(lines) with patch.object(subprocess, "run") as mock_run: mock_run.return_value = MagicMock( stdout=lspci_output, returncode=0, ) module.check_gpu_linux(context) # subprocess.run called once with ["lspci"], not piped to grep mock_run.assert_called_once() assert context["_nvidia_gpu"] is True class TestCheckGpuWindows: """Verify check_gpu_windows uses secure subprocess invocation.""" def test_calls_subprocess_run_with_list_args(self): """check_gpu_windows must use subprocess.run with a list.""" module = _load_pre_prompt_module() context = {} wmic_output = "Name" + chr(10) + "NVIDIA GeForce RTX 3080" with patch.object(subprocess, "run") as mock_run: mock_run.return_value = MagicMock( stdout=wmic_output, returncode=0, ) module.check_gpu_windows(context) mock_run.assert_called_once() call_args = mock_run.call_args cmd_arg = ( call_args[0][0] if call_args[0] else call_args[1].get("args") ) assert isinstance(cmd_arg, list), ( f"subprocess.run must be called with a list, " f"got {type(cmd_arg)}" ) assert cmd_arg == [ "wmic", "path", "win32_VideoController", "get", "name", ] def test_does_not_use_shell_true(self): """check_gpu_windows must not pass shell=True.""" module = _load_pre_prompt_module() context = {} with patch.object(subprocess, "run") as mock_run: mock_run.return_value = MagicMock(stdout="Name", returncode=0) module.check_gpu_windows(context) call_kwargs = mock_run.call_args[1] if mock_run.call_args[1] else {} assert call_kwargs.get("shell") is not True, ( "check_gpu_windows must not use shell=True" ) def test_detects_nvidia_gpu(self): """check_gpu_windows correctly detects NVIDIA GPU.""" module = _load_pre_prompt_module() context = {} wmic_output = "Name" + chr(10) + "NVIDIA GeForce RTX 3080" with patch.object(subprocess, "run") as mock_run: mock_run.return_value = MagicMock( stdout=wmic_output, returncode=0, ) module.check_gpu_windows(context) assert context["_nvidia_gpu"] is True assert context["_amd_gpu"] is False assert context["enable_gpu"] is True def test_detects_amd_gpu(self): """check_gpu_windows correctly detects AMD GPU.""" module = _load_pre_prompt_module() context = {} wmic_output = "Name" + chr(10) + "AMD Radeon RX 7900 XTX" with patch.object(subprocess, "run") as mock_run: mock_run.return_value = MagicMock( stdout=wmic_output, returncode=0, ) module.check_gpu_windows(context) assert context["_amd_gpu"] is True assert context["_nvidia_gpu"] is False assert context["enable_gpu"] is True def test_detects_radeon_gpu(self): """check_gpu_windows detects Radeon-branded AMD GPUs.""" module = _load_pre_prompt_module() context = {} wmic_output = "Name" + chr(10) + "Radeon RX 580" with patch.object(subprocess, "run") as mock_run: mock_run.return_value = MagicMock( stdout=wmic_output, returncode=0, ) module.check_gpu_windows(context) assert context["_amd_gpu"] is True assert context["_nvidia_gpu"] is False assert context["enable_gpu"] is True def test_no_gpu_detected(self): """check_gpu_windows handles no GPU detection.""" module = _load_pre_prompt_module() context = {} wmic_output = "Name" + chr(10) + "Microsoft Basic Display Adapter" with patch.object(subprocess, "run") as mock_run: mock_run.return_value = MagicMock( stdout=wmic_output, returncode=0, ) module.check_gpu_windows(context) assert context["_nvidia_gpu"] is False assert context["_amd_gpu"] is False assert context["enable_gpu"] is False class TestOpenFileLocationPathValidation: """Verify open_file_location uses PathValidator for defense-in-depth.""" @patch("local_deep_research.research_library.utils.PathValidator") @patch("local_deep_research.research_library.utils.subprocess") def test_calls_path_validator_before_opening( self, mock_subprocess, mock_validator_cls ): """open_file_location must validate path via PathValidator.""" from local_deep_research.research_library.utils import ( open_file_location, ) mock_validated_path = MagicMock() mock_validated_path.parent = Path("/safe/directory") mock_validator_cls.validate_local_filesystem_path.return_value = ( mock_validated_path ) mock_subprocess.run.return_value = MagicMock(returncode=0) with patch( "local_deep_research.research_library.utils.sys" ) as mock_sys: mock_sys.platform = "linux" result = open_file_location("/safe/directory/file.txt") mock_validator_cls.validate_local_filesystem_path.assert_called_once_with( "/safe/directory/file.txt" ) assert result is True @patch("local_deep_research.research_library.utils.PathValidator") @patch("local_deep_research.research_library.utils.subprocess") def test_uses_validated_path_parent_for_folder( self, mock_subprocess, mock_validator_cls ): """open_file_location must use validated path parent, not raw input.""" from local_deep_research.research_library.utils import ( open_file_location, ) mock_validated_path = MagicMock() mock_validated_path.parent = Path("/validated/parent") mock_validator_cls.validate_local_filesystem_path.return_value = ( mock_validated_path ) mock_subprocess.run.return_value = MagicMock(returncode=0) with patch( "local_deep_research.research_library.utils.sys" ) as mock_sys: mock_sys.platform = "linux" open_file_location("/some/raw/path/file.txt") mock_subprocess.run.assert_called_once() call_args = mock_subprocess.run.call_args[0][0] assert call_args == ["xdg-open", str(Path("/validated/parent"))] @patch("local_deep_research.research_library.utils.PathValidator") def test_returns_false_on_path_validation_failure(self, mock_validator_cls): """open_file_location returns False if PathValidator rejects path.""" from local_deep_research.research_library.utils import ( open_file_location, ) mock_validator_cls.validate_local_filesystem_path.side_effect = ( ValueError("Cannot access system directories") ) result = open_file_location("/etc/shadow") assert result is False mock_validator_cls.validate_local_filesystem_path.assert_called_once_with( "/etc/shadow" ) @patch("local_deep_research.research_library.utils.PathValidator") def test_blocks_path_traversal_via_validator(self, mock_validator_cls): """open_file_location blocks path traversal via PathValidator.""" from local_deep_research.research_library.utils import ( open_file_location, ) mock_validator_cls.validate_local_filesystem_path.side_effect = ( ValueError("Path traversal patterns not allowed") ) result = open_file_location("../../etc/passwd") assert result is False mock_validator_cls.validate_local_filesystem_path.assert_called_once_with( "../../etc/passwd" )