""" Tests for SafeExecutor (PR3): path validation, env filtering, subprocess mocking, timeout, exit_code. All tests use monkeypatched subprocess — no real scripts executed. """ from __future__ import annotations import asyncio from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest def test_path_traversal_rejected(): """Script path with .. is rejected.""" import app.safe_executor as se with pytest.raises(se.ScriptNotAllowedError, match="traversal"): se._validate_script_path("ops/../etc/passwd") def test_absolute_path_rejected(): """Absolute script path is rejected.""" import app.safe_executor as se with pytest.raises(se.ScriptNotAllowedError, match="Absolute"): se._validate_script_path("/bin/bash") def test_non_allowlisted_script_rejected(): """Unknown script path is rejected even if no traversal.""" import app.safe_executor as se with pytest.raises(se.ScriptNotAllowedError, match="allowlist"): se._validate_script_path("ops/custom_hack.sh") def test_allowlisted_script_resolves(): """Allowlisted script resolves to a Path under REPO_ROOT without error.""" import app.safe_executor as se resolved = se._validate_script_path("ops/preflight_sofiia_console.sh") assert resolved.name == "preflight_sofiia_console.sh" assert str(se._REPO_ROOT) in str(resolved) def test_env_filter_drops_non_allowlisted_keys(): """Non-allowlisted env keys are dropped and recorded.""" import app.safe_executor as se filtered, dropped = se._filter_env({ "STRICT": "1", "LD_PRELOAD": "/evil.so", "PYTHONPATH": "/hack", "SOFIIA_URL": "http://localhost:8002", }) assert "STRICT" in filtered assert "SOFIIA_URL" in filtered assert "LD_PRELOAD" not in filtered assert "PYTHONPATH" not in filtered assert "LD_PRELOAD" in dropped assert "PYTHONPATH" in dropped def test_env_filter_empty(): """Empty env dict produces empty filtered + no dropped.""" import app.safe_executor as se filtered, dropped = se._filter_env({}) assert filtered == {} assert dropped == [] def _fake_proc(returncode: int = 0, stdout: bytes = b"ok", stderr: bytes = b"") -> MagicMock: """Return a mock process where communicate() returns (stdout, stderr).""" proc = MagicMock() proc.returncode = returncode proc.kill = MagicMock() proc.wait = AsyncMock(return_value=None) async def _communicate(): return stdout, stderr proc.communicate = _communicate return proc def test_run_script_success(sofiia_module, monkeypatch): """Allowlisted script with exit_code=0 returns ok=True, exit_code=0, stdout_tail.""" import app.safe_executor as se loop = asyncio.get_event_loop() async def run(): with patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec: mock_exec.return_value = _fake_proc(returncode=0, stdout=b"PASS\n", stderr=b"") return await se.run_script("ops/preflight_sofiia_console.sh", env={"STRICT": "1"}) result = loop.run_until_complete(run()) assert result["ok"] is True assert result["exit_code"] == 0 assert "PASS" in result["stdout_tail"] assert result["timeout"] is False def test_run_script_nonzero_exit(sofiia_module, monkeypatch): """Script with exit_code=1 returns ok=False, exit_code=1.""" import app.safe_executor as se loop = asyncio.get_event_loop() async def run(): with patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec: mock_exec.return_value = _fake_proc(returncode=1, stderr=b"FAIL\n") return await se.run_script("ops/preflight_sofiia_console.sh") result = loop.run_until_complete(run()) assert result["ok"] is False assert result["exit_code"] == 1 assert result["timeout"] is False def test_run_script_timeout(sofiia_module, monkeypatch): """Script that hangs beyond timeout returns timeout=True, ok=False.""" import app.safe_executor as se loop = asyncio.get_event_loop() async def run(): async def slow_proc(*args, **kwargs): proc = MagicMock() proc.kill = MagicMock() proc.wait = AsyncMock(return_value=None) async def _hang(): await asyncio.sleep(9999) return b"", b"" proc.communicate = _hang return proc with patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec: mock_exec.side_effect = slow_proc return await se.run_script("ops/preflight_sofiia_console.sh", timeout_s=1) result = loop.run_until_complete(run()) assert result["timeout"] is True assert result["ok"] is False def test_run_script_timeout_clamped(sofiia_module, monkeypatch): """Timeout values above _TIMEOUT_MAX_S are clamped.""" import app.safe_executor as se loop = asyncio.get_event_loop() captured_timeout: list = [] async def run(): async def fake_wait_for(coro, timeout): captured_timeout.append(timeout) return await asyncio.wait_for(coro, timeout=999) with patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec: mock_exec.return_value = _fake_proc(returncode=0) with patch("asyncio.wait_for", side_effect=fake_wait_for): return await se.run_script( "ops/preflight_sofiia_console.sh", timeout_s=9999, ) loop.run_until_complete(run()) if captured_timeout: assert captured_timeout[0] <= se._TIMEOUT_MAX_S def test_run_script_env_key_not_in_allowlist_dropped(sofiia_module, monkeypatch): """Non-allowlisted env keys are dropped; result still has dropped_env_keys.""" import app.safe_executor as se loop = asyncio.get_event_loop() async def run(): with patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec: mock_exec.return_value = _fake_proc(returncode=0) return await se.run_script( "ops/preflight_sofiia_console.sh", env={"STRICT": "1", "EVIL_KEY": "bad"}, ) result = loop.run_until_complete(run()) assert result.get("dropped_env_keys") is not None assert "EVIL_KEY" in result["dropped_env_keys"] def test_run_script_non_allowlisted_returns_error(sofiia_module): """Non-allowlisted script returns ok=False without executing anything.""" import app.safe_executor as se loop = asyncio.get_event_loop() async def run(): return await se.run_script("ops/some_random_script.sh") result = loop.run_until_complete(run()) assert result["ok"] is False assert "allowlist" in result.get("error", "").lower()