"""Tests for marketing_intelligence.mcp.tools.browser SessionManager logic.""" from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from marketing_intelligence.mcp.tools.browser import SessionManager from marketing_intelligence.mcp.tools.responses import ( BrowsersResponse, ProfilesResponse, SessionBehaviorResponse, SessionClosedResponse, SessionResponse, ) def _mock_session( session_id: str = "sid1", engine: str = "camoufox", profile_name: str = "desktop", ) -> MagicMock: s = MagicMock() s.session_id = session_id s.engine = engine s.profile_name = profile_name s.human_behavior = True s.delay_factor = 1.0 s.scroll_speed = "normal" return s class TestBrowsersCapabilities: def test_returns_browsers_response(self) -> None: r = SessionManager.browsers() assert isinstance(r, BrowsersResponse) def test_contains_playwright_nodriver_drissionpage(self) -> None: names = [b.name for b in SessionManager.browsers().browsers] assert "playwright" in names assert "nodriver" in names assert "drissionpage" in names def test_playwright_has_camoufox_chromium_webkit(self) -> None: pw = next( b for b in SessionManager.browsers().browsers if b.name == "playwright" ) stealth_names = [s.name for s in pw.stealths] assert "camoufox" in stealth_names assert "chromium" in stealth_names assert "webkit" in stealth_names def test_tip_is_set(self) -> None: assert SessionManager.browsers().tip is not None def test_nodriver_has_scrape_video_not_supported_note(self) -> None: nd = next(b for b in SessionManager.browsers().browsers if b.name == "nodriver") assert nd.note is not None class TestProfiles: def test_returns_profiles_response(self) -> None: with patch( "marketing_intelligence.mcp.tools.browser.list_profiles", return_value=["desktop", "mobile_ios"], ): r = SessionManager().profiles() assert isinstance(r, ProfilesResponse) def test_delegates_to_list_profiles(self) -> None: with patch( "marketing_intelligence.mcp.tools.browser.list_profiles", return_value=["desktop"], ): r = SessionManager().profiles() assert "desktop" in r.profiles class TestCreateSession: def test_create_without_proxy_returns_session_response(self) -> None: with ( patch("marketing_intelligence.mcp.tools.browser.settings") as mock_settings, patch( "marketing_intelligence.mcp.tools.browser.create_session", return_value=_mock_session(), ), ): mock_settings.proxy_list = [] mock_settings.proxy_lambda_name = None r = SessionManager().create() assert isinstance(r, SessionResponse) assert r.success is True assert r.session_id == "sid1" def test_playwright_browser_sets_stealth(self) -> None: with ( patch("marketing_intelligence.mcp.tools.browser.settings") as mock_settings, patch( "marketing_intelligence.mcp.tools.browser.create_session", return_value=_mock_session(), ), ): mock_settings.proxy_list = [] mock_settings.proxy_lambda_name = None r = SessionManager().create(browser="playwright", stealth="chromium") assert r.stealth == "chromium" def test_non_playwright_browser_has_no_stealth(self) -> None: with ( patch("marketing_intelligence.mcp.tools.browser.settings") as mock_settings, patch( "marketing_intelligence.mcp.tools.browser.create_session", return_value=_mock_session(engine="chrome"), ), ): mock_settings.proxy_list = [] mock_settings.proxy_lambda_name = None r = SessionManager().create(browser="nodriver") assert r.stealth is None def test_proxy_acquired_when_proxy_list_configured(self) -> None: mock_ps = MagicMock() mock_ps.to_playwright_dict.return_value = {"server": "http://proxy:8080"} mock_ps.country = "gb" mock_proxy_mgr = MagicMock() mock_proxy_mgr.acquire.return_value = mock_ps with ( patch("marketing_intelligence.mcp.tools.browser.settings") as mock_settings, patch( "marketing_intelligence.mcp.tools.browser.ProxyManager", return_value=mock_proxy_mgr, ), patch( "marketing_intelligence.mcp.tools.browser.create_session", return_value=_mock_session(), ), ): mock_settings.proxy_list = ["http://proxy:8080"] mock_settings.proxy_lambda_name = None r = SessionManager().create() assert r.success is True mock_proxy_mgr.acquire.assert_called_once() def test_proxy_acquired_when_lambda_name_configured(self) -> None: mock_ps = MagicMock() mock_ps.to_playwright_dict.return_value = {"server": "http://proxy:8080"} mock_ps.country = None mock_proxy_mgr = MagicMock() mock_proxy_mgr.acquire.return_value = mock_ps with ( patch("marketing_intelligence.mcp.tools.browser.settings") as mock_settings, patch( "marketing_intelligence.mcp.tools.browser.ProxyManager", return_value=mock_proxy_mgr, ), patch( "marketing_intelligence.mcp.tools.browser.create_session", return_value=_mock_session(), ), ): mock_settings.proxy_list = [] mock_settings.proxy_lambda_name = "proxy-fn" r = SessionManager().create() assert r.success is True mock_proxy_mgr.acquire.assert_called_once() def test_proxy_failure_does_not_abort_session_creation(self) -> None: mock_proxy_mgr = MagicMock() mock_proxy_mgr.acquire.side_effect = RuntimeError("no proxy available") with ( patch("marketing_intelligence.mcp.tools.browser.settings") as mock_settings, patch( "marketing_intelligence.mcp.tools.browser.ProxyManager", return_value=mock_proxy_mgr, ), patch( "marketing_intelligence.mcp.tools.browser.create_session", return_value=_mock_session(), ), ): mock_settings.proxy_list = ["http://proxy:8080"] mock_settings.proxy_lambda_name = None r = SessionManager().create() assert r.success is True def test_create_passes_params_to_create_session(self) -> None: captured: dict[str, Any] = {} def _capture(**kwargs: Any) -> MagicMock: captured.update(kwargs) return _mock_session() with ( patch("marketing_intelligence.mcp.tools.browser.settings") as mock_settings, patch( "marketing_intelligence.mcp.tools.browser.create_session", side_effect=_capture, ), ): mock_settings.proxy_list = [] mock_settings.proxy_lambda_name = None SessionManager().create( browser="playwright", stealth="camoufox", profile="mobile_ios", human_behavior=False, delay_factor=2.5, scroll_speed="fast", ) assert captured["engine"] == "camoufox" assert captured["profile_name"] == "mobile_ios" assert captured["human_behavior"] is False assert captured["delay_factor"] == 2.5 assert captured["scroll_speed"] == "fast" class TestCloseSession: def test_session_found_returns_success(self) -> None: with ( patch( "marketing_intelligence.mcp.tools.browser.delete_session", return_value=True, ), patch("marketing_intelligence.mcp.tools.browser.asyncio.ensure_future"), patch("marketing_intelligence.mcp.tools.browser._pool") as mock_pool, ): mock_pool.close = AsyncMock() r = SessionManager().close("sid1") assert isinstance(r, SessionClosedResponse) assert r.success is True assert r.session_id == "sid1" assert r.error is None def test_session_not_found_returns_failure(self) -> None: with patch( "marketing_intelligence.mcp.tools.browser.delete_session", return_value=False, ): r = SessionManager().close("missing_sid") assert r.success is False assert r.session_id == "missing_sid" assert "not found" in (r.error or "") def test_ensure_future_called_when_found(self) -> None: with ( patch( "marketing_intelligence.mcp.tools.browser.delete_session", return_value=True, ), patch( "marketing_intelligence.mcp.tools.browser.asyncio.ensure_future" ) as mock_ef, patch("marketing_intelligence.mcp.tools.browser._pool") as mock_pool, ): mock_pool.close = AsyncMock() SessionManager().close("sid1") mock_ef.assert_called_once() class TestUpdateTactics: def test_session_not_found_returns_failure(self) -> None: with patch( "marketing_intelligence.mcp.tools.browser.get_session", return_value=None ): r = SessionManager().update_tactics("bad_sid") assert isinstance(r, SessionBehaviorResponse) assert r.success is False assert "not found" in (r.error or "") def test_session_found_applies_updates(self) -> None: updated = MagicMock() updated.delay_factor = 2.0 updated.scroll_speed = "fast" updated.human_behavior = False with ( patch( "marketing_intelligence.mcp.tools.browser.get_session", return_value=MagicMock(), ), patch( "marketing_intelligence.mcp.tools.browser.update_session", return_value=updated, ), ): r = SessionManager().update_tactics( "sid1", delay_factor=2.0, scroll_speed="fast", human_behavior=False ) assert r.success is True assert r.delay_factor == 2.0 assert r.scroll_speed == "fast" assert r.human_behavior is False def test_update_session_returns_none_gives_failure(self) -> None: with ( patch( "marketing_intelligence.mcp.tools.browser.get_session", return_value=MagicMock(), ), patch( "marketing_intelligence.mcp.tools.browser.update_session", return_value=None, ), ): r = SessionManager().update_tactics("sid1", delay_factor=1.5) assert r.success is False assert "not found" in (r.error or "") def test_none_params_not_passed_to_update_session(self) -> None: updated = MagicMock() updated.delay_factor = 2.0 updated.scroll_speed = "normal" updated.human_behavior = True captured: dict[str, Any] = {} def _capture(sid: str, **kwargs: Any) -> MagicMock: captured.update(kwargs) return updated with ( patch( "marketing_intelligence.mcp.tools.browser.get_session", return_value=MagicMock(), ), patch( "marketing_intelligence.mcp.tools.browser.update_session", side_effect=_capture, ), ): SessionManager().update_tactics("sid1", delay_factor=2.0) assert "delay_factor" in captured assert "scroll_speed" not in captured assert "human_behavior" not in captured def test_all_none_params_excluded(self) -> None: updated = MagicMock() updated.delay_factor = 1.0 updated.scroll_speed = "normal" updated.human_behavior = True captured: dict[str, Any] = {} def _capture(sid: str, **kwargs: Any) -> MagicMock: captured.update(kwargs) return updated with ( patch( "marketing_intelligence.mcp.tools.browser.get_session", return_value=MagicMock(), ), patch( "marketing_intelligence.mcp.tools.browser.update_session", side_effect=_capture, ), ): SessionManager().update_tactics("sid1") assert captured == {}