Skip to content

Commit ab620fc

Browse files
fix(login): forward all browser config params during --login (#262)
The --login flow was missing slow_mo, user_agent, and viewport params when constructing BrowserManager, unlike the server runtime path in drivers/browser.py:_make_browser(). This caused config like --slow-mo, --user-agent, and --viewport to be silently ignored during login. Add comprehensive tests verifying every BrowserManager param is forwarded.
1 parent 2722bdc commit ab620fc

2 files changed

Lines changed: 143 additions & 50 deletions

File tree

linkedin_mcp_server/setup.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,18 @@ async def interactive_login(
5353
if config.browser.chrome_path:
5454
launch_options["executable_path"] = config.browser.chrome_path
5555

56+
viewport = {
57+
"width": config.browser.viewport_width,
58+
"height": config.browser.viewport_height,
59+
}
60+
5661
async with BrowserManager(
57-
user_data_dir=user_data_dir, headless=False, **launch_options
62+
user_data_dir=user_data_dir,
63+
headless=False,
64+
slow_mo=config.browser.slow_mo,
65+
user_agent=config.browser.user_agent,
66+
viewport=viewport,
67+
**launch_options,
5868
) as browser:
5969
# Warm up browser to appear more human-like and avoid security checkpoints
6070
if warm_up:

tests/test_setup.py

Lines changed: 132 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -31,34 +31,47 @@ def _make_browser(*, export_cookies: bool) -> MagicMock:
3131
return browser
3232

3333

34-
@pytest.mark.asyncio
35-
async def test_interactive_login_writes_source_state_when_cookie_export_succeeds(
36-
monkeypatch, tmp_path, capsys
37-
):
38-
browser = _make_browser(export_cookies=True)
39-
write_source_state = MagicMock(
40-
return_value=SimpleNamespace(login_generation="gen-123")
41-
)
42-
43-
monkeypatch.setattr("linkedin_mcp_server.setup.get_config", lambda: AppConfig())
34+
def _patch_login_deps(
35+
monkeypatch,
36+
*,
37+
browser_factory,
38+
config: AppConfig | None = None,
39+
write_source_state: MagicMock | None = None,
40+
) -> None:
41+
"""Patch all interactive_login dependencies in one place."""
4442
monkeypatch.setattr(
45-
"linkedin_mcp_server.setup.BrowserManager",
46-
lambda **kwargs: _BrowserContextManager(browser),
43+
"linkedin_mcp_server.setup.get_config", lambda: config or AppConfig()
4744
)
45+
monkeypatch.setattr("linkedin_mcp_server.setup.BrowserManager", browser_factory)
4846
monkeypatch.setattr("linkedin_mcp_server.setup.warm_up_browser", AsyncMock())
4947
monkeypatch.setattr(
5048
"linkedin_mcp_server.setup.resolve_remember_me_prompt",
5149
AsyncMock(return_value=False),
5250
)
51+
monkeypatch.setattr("linkedin_mcp_server.setup.wait_for_manual_login", AsyncMock())
5352
monkeypatch.setattr(
54-
"linkedin_mcp_server.setup.wait_for_manual_login",
55-
AsyncMock(),
56-
)
57-
monkeypatch.setattr(
58-
"linkedin_mcp_server.setup.write_source_state", write_source_state
53+
"linkedin_mcp_server.setup.write_source_state",
54+
write_source_state
55+
or MagicMock(return_value=SimpleNamespace(login_generation="gen-1")),
5956
)
6057
monkeypatch.setattr("linkedin_mcp_server.setup.asyncio.sleep", AsyncMock())
6158

59+
60+
@pytest.mark.asyncio
61+
async def test_interactive_login_writes_source_state_when_cookie_export_succeeds(
62+
monkeypatch, tmp_path, capsys
63+
):
64+
browser = _make_browser(export_cookies=True)
65+
write_source_state = MagicMock(
66+
return_value=SimpleNamespace(login_generation="gen-123")
67+
)
68+
69+
_patch_login_deps(
70+
monkeypatch,
71+
browser_factory=lambda **kwargs: _BrowserContextManager(browser),
72+
write_source_state=write_source_state,
73+
)
74+
6275
assert await interactive_login(tmp_path / "profile") is True
6376

6477
browser.export_cookies.assert_awaited_once_with(
@@ -77,24 +90,11 @@ async def test_interactive_login_returns_false_when_cookie_export_fails(
7790
browser = _make_browser(export_cookies=False)
7891
write_source_state = MagicMock()
7992

80-
monkeypatch.setattr("linkedin_mcp_server.setup.get_config", lambda: AppConfig())
81-
monkeypatch.setattr(
82-
"linkedin_mcp_server.setup.BrowserManager",
83-
lambda **kwargs: _BrowserContextManager(browser),
93+
_patch_login_deps(
94+
monkeypatch,
95+
browser_factory=lambda **kwargs: _BrowserContextManager(browser),
96+
write_source_state=write_source_state,
8497
)
85-
monkeypatch.setattr("linkedin_mcp_server.setup.warm_up_browser", AsyncMock())
86-
monkeypatch.setattr(
87-
"linkedin_mcp_server.setup.resolve_remember_me_prompt",
88-
AsyncMock(return_value=False),
89-
)
90-
monkeypatch.setattr(
91-
"linkedin_mcp_server.setup.wait_for_manual_login",
92-
AsyncMock(),
93-
)
94-
monkeypatch.setattr(
95-
"linkedin_mcp_server.setup.write_source_state", write_source_state
96-
)
97-
monkeypatch.setattr("linkedin_mcp_server.setup.asyncio.sleep", AsyncMock())
9898

9999
assert await interactive_login(tmp_path / "profile") is False
100100

@@ -122,22 +122,105 @@ def fake_browser_manager(**kwargs):
122122
config = AppConfig()
123123
config.browser.chrome_path = "/custom/chrome"
124124

125-
monkeypatch.setattr("linkedin_mcp_server.setup.get_config", lambda: config)
126-
monkeypatch.setattr(
127-
"linkedin_mcp_server.setup.BrowserManager", fake_browser_manager
128-
)
129-
monkeypatch.setattr("linkedin_mcp_server.setup.warm_up_browser", AsyncMock())
130-
monkeypatch.setattr(
131-
"linkedin_mcp_server.setup.resolve_remember_me_prompt",
132-
AsyncMock(return_value=False),
133-
)
134-
monkeypatch.setattr("linkedin_mcp_server.setup.wait_for_manual_login", AsyncMock())
135-
monkeypatch.setattr(
136-
"linkedin_mcp_server.setup.write_source_state",
137-
MagicMock(return_value=SimpleNamespace(login_generation="gen-1")),
138-
)
139-
monkeypatch.setattr("linkedin_mcp_server.setup.asyncio.sleep", AsyncMock())
125+
_patch_login_deps(monkeypatch, browser_factory=fake_browser_manager, config=config)
140126

141127
await interactive_login(tmp_path / "profile")
142128

143129
assert captured_kwargs.get("executable_path") == "/custom/chrome"
130+
131+
132+
@pytest.mark.asyncio
133+
async def test_interactive_login_forwards_all_browser_params(monkeypatch, tmp_path):
134+
"""All browser config params must reach BrowserManager during --login."""
135+
browser = _make_browser(export_cookies=True)
136+
captured_kwargs: dict = {}
137+
138+
def fake_browser_manager(**kwargs):
139+
captured_kwargs.update(kwargs)
140+
return _BrowserContextManager(browser)
141+
142+
config = AppConfig()
143+
config.browser.chrome_path = "/custom/chrome"
144+
config.browser.slow_mo = 250
145+
config.browser.user_agent = "CustomAgent/1.0"
146+
config.browser.viewport_width = 1920
147+
config.browser.viewport_height = 1080
148+
149+
_patch_login_deps(monkeypatch, browser_factory=fake_browser_manager, config=config)
150+
151+
profile = tmp_path / "profile"
152+
await interactive_login(profile)
153+
154+
assert captured_kwargs["user_data_dir"] == profile
155+
assert captured_kwargs["headless"] is False
156+
assert captured_kwargs["slow_mo"] == 250
157+
assert captured_kwargs["user_agent"] == "CustomAgent/1.0"
158+
assert captured_kwargs["viewport"] == {"width": 1920, "height": 1080}
159+
assert captured_kwargs["executable_path"] == "/custom/chrome"
160+
161+
162+
@pytest.mark.asyncio
163+
async def test_interactive_login_passes_slow_mo_to_browser_manager(
164+
monkeypatch, tmp_path
165+
):
166+
"""When config.browser.slow_mo is set, it must reach BrowserManager."""
167+
browser = _make_browser(export_cookies=True)
168+
captured_kwargs: dict = {}
169+
170+
def fake_browser_manager(**kwargs):
171+
captured_kwargs.update(kwargs)
172+
return _BrowserContextManager(browser)
173+
174+
config = AppConfig()
175+
config.browser.slow_mo = 250
176+
177+
_patch_login_deps(monkeypatch, browser_factory=fake_browser_manager, config=config)
178+
179+
await interactive_login(tmp_path / "profile")
180+
181+
assert captured_kwargs.get("slow_mo") == 250
182+
183+
184+
@pytest.mark.asyncio
185+
async def test_interactive_login_passes_user_agent_to_browser_manager(
186+
monkeypatch, tmp_path
187+
):
188+
"""When config.browser.user_agent is set, it must reach BrowserManager."""
189+
browser = _make_browser(export_cookies=True)
190+
captured_kwargs: dict = {}
191+
192+
def fake_browser_manager(**kwargs):
193+
captured_kwargs.update(kwargs)
194+
return _BrowserContextManager(browser)
195+
196+
config = AppConfig()
197+
config.browser.user_agent = "CustomAgent/1.0"
198+
199+
_patch_login_deps(monkeypatch, browser_factory=fake_browser_manager, config=config)
200+
201+
await interactive_login(tmp_path / "profile")
202+
203+
assert captured_kwargs.get("user_agent") == "CustomAgent/1.0"
204+
205+
206+
@pytest.mark.asyncio
207+
async def test_interactive_login_passes_viewport_to_browser_manager(
208+
monkeypatch, tmp_path
209+
):
210+
"""Non-default viewport_width/viewport_height must reach BrowserManager as viewport."""
211+
browser = _make_browser(export_cookies=True)
212+
captured_kwargs: dict = {}
213+
214+
def fake_browser_manager(**kwargs):
215+
captured_kwargs.update(kwargs)
216+
return _BrowserContextManager(browser)
217+
218+
config = AppConfig()
219+
config.browser.viewport_width = 1920
220+
config.browser.viewport_height = 1080
221+
222+
_patch_login_deps(monkeypatch, browser_factory=fake_browser_manager, config=config)
223+
224+
await interactive_login(tmp_path / "profile")
225+
226+
assert captured_kwargs.get("viewport") == {"width": 1920, "height": 1080}

0 commit comments

Comments
 (0)