"""Unit tests for readiness probe service.""" from unittest.mock import MagicMock, patch from src.services.shared.readiness_probe import execute_probe class TestExecuteProbe: """Tests for execute_probe function.""" @patch("subprocess.run") async def test_probe_succeeds_first_attempt(self, mock_run) -> None: mock_run.return_value = MagicMock( returncode=0, stdout="healthy", stderr="", ) result, logs = await execute_probe("container-123", "curl -f http://localhost:8080") assert result is True assert any("Success" in log for log in logs) mock_run.assert_called_once_with( ["docker", "exec", "container-123", "sh", "-c", "curl -f http://localhost:8080"], capture_output=True, text=True, timeout=2, ) @patch("subprocess.run") async def test_probe_fails_then_succeeds(self, mock_run) -> None: mock_run.side_effect = [ MagicMock(returncode=1, stdout="", stderr="Connection refused"), MagicMock(returncode=1, stdout="", stderr="Connection refused"), MagicMock(returncode=0, stdout="healthy", stderr=""), ] result, logs = await execute_probe("container-123", "curl -f http://localhost:8080", timeout=10, interval=0.1) assert result is True assert mock_run.call_count == 3 assert any("Attempt 1: Failed" in log for log in logs) assert any("Attempt 3: Success" in log for log in logs) @patch("subprocess.run") async def test_probe_times_out(self, mock_run) -> None: mock_run.return_value = MagicMock( returncode=1, stdout="", stderr="Connection refused", ) result, logs = await execute_probe("container-123", "curl -f http://localhost:8080", timeout=0.5, interval=0.2) assert result is False assert any("timed out" in log.lower() for log in logs) @patch("subprocess.run") async def test_probe_command_not_found(self, mock_run) -> None: mock_run.return_value = MagicMock( returncode=127, stdout="", stderr="command not found", ) result, logs = await execute_probe("container-123", "nonexistent-command", timeout=1, interval=0.3) assert result is False assert any("exit code 127" in log for log in logs) @patch("subprocess.run") async def test_probe_exception(self, mock_run) -> None: mock_run.side_effect = OSError("Docker not available") result, logs = await execute_probe("container-123", "curl http://localhost", timeout=1, interval=0.3) assert result is False assert any("Error" in log for log in logs) @patch("subprocess.run") async def test_probe_with_special_characters(self, mock_run) -> None: mock_run.return_value = MagicMock( returncode=0, stdout="", stderr="", ) cmd = "bash -c 'echo \"hello world\" && exit 0'" await execute_probe("container-123", cmd) call_args = mock_run.call_args assert cmd in call_args[0][0] @patch("subprocess.run") async def test_probe_captures_stdout(self, mock_run) -> None: mock_run.return_value = MagicMock( returncode=0, stdout="Server is ready\\nVersion: 1.0", stderr="", ) result, logs = await execute_probe("container-123", "cat /app/status") assert result is True assert any("Server is ready" in log for log in logs) class TestIntegrationScenarios: """Integration-style tests with realistic scenarios.""" @patch("subprocess.run") async def test_web_server_probe(self, mock_run) -> None: """Test typical web server health check.""" mock_run.side_effect = [ MagicMock(returncode=1, stdout="", stderr=""), MagicMock(returncode=1, stdout="", stderr=""), MagicMock(returncode=1, stdout="", stderr=""), MagicMock(returncode=0, stdout="OK", stderr=""), ] result, logs = await execute_probe( "web-container", "curl -f http://localhost:8080/health", timeout=10, interval=0.2, ) assert result is True assert mock_run.call_count == 4 @patch("subprocess.run") async def test_command_probe(self, mock_run) -> None: """Test command availability check.""" mock_run.return_value = MagicMock( returncode=0, stdout="opencode 1.0.0", stderr="", ) result, logs = await execute_probe( "tool-container", "which opencode && opencode --version", timeout=30, interval=2, ) assert result is True assert any("opencode 1.0.0" in log for log in logs) @patch("subprocess.run") async def test_database_probe(self, mock_run) -> None: """Test database readiness check.""" mock_run.side_effect = [ MagicMock(returncode=1, stdout="", stderr=""), MagicMock(returncode=1, stdout="", stderr=""), MagicMock(returncode=0, stdout="/var/run/postgresql:5432 - accepting connections", stderr=""), ] result, logs = await execute_probe( "db-container", "pg_isready -U postgres", timeout=10, interval=0.3, ) assert result is True assert mock_run.call_count == 3 @patch("subprocess.run") async def test_file_probe(self, mock_run) -> None: """Test file existence check.""" mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="") result, logs = await execute_probe( "app-container", "[ -f /app/ready ]", timeout=10, interval=1, ) assert result is True @patch("subprocess.run") async def test_slow_starting_service(self, mock_run) -> None: """Test service that takes time to start.""" # Simulate 5 failures before success side_effects = [MagicMock(returncode=1, stdout="", stderr="")] * 5 side_effects.append(MagicMock(returncode=0, stdout="Ready", stderr="")) mock_run.side_effect = side_effects result, logs = await execute_probe( "slow-container", "curl -f http://localhost:8080", timeout=10, interval=0.2, ) assert result is True assert mock_run.call_count == 6 assert any("Attempt 6: Success" in log for log in logs) @patch("subprocess.run") async def test_zero_timeout_immediate_return(self, mock_run) -> None: mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="") result, logs = await execute_probe( "container", "test", timeout=0, interval=1, ) assert result is False assert any("timed out" in log.lower() for log in logs)