File size: 13,977 Bytes
cc036ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
"""
Coverage expansion tests for browser automation tool.

Tests cover critical code paths in:
- tools/browser_tool.py: Browser session management, CDP operations
- Navigation, screenshots, form filling, scraping
- Governance enforcement for browser operations
- Session lifecycle (start, close, cleanup)

Target: Cover critical paths (happy path + error paths) to increase coverage.
Uses extensive mocking to avoid Playwright/browser dependencies.
"""

import pytest
from unittest.mock import Mock, patch, MagicMock, AsyncMock
from datetime import datetime
import asyncio

from tools.browser_tool import BrowserSession, BrowserSessionManager


class TestBrowserSessionCoverage:
    """Coverage expansion for BrowserSession class."""

    @pytest.fixture
    def browser_session(self):
        """Get browser session instance."""
        return BrowserSession(
            session_id="test-session-123",
            user_id="user-123",
            agent_id="agent-123",
            headless=True,
            browser_type="chromium"
        )

    # Test: BrowserSession initialization
    def test_browser_session_init(self, browser_session):
        """Browser session initializes correctly."""
        assert browser_session.session_id == "test-session-123"
        assert browser_session.user_id == "user-123"
        assert browser_session.agent_id == "agent-123"
        assert browser_session.headless == True
        assert browser_session.browser_type == "chromium"
        assert browser_session.playwright is None
        assert browser_session.browser is None
        assert browser_session.context is None
        assert browser_session.page is None

    def test_browser_session_init_firefox(self):
        """Browser session with Firefox type."""
        session = BrowserSession(
            session_id="ff-session",
            user_id="user-123",
            browser_type="firefox"
        )
        assert session.browser_type == "firefox"

    def test_browser_session_init_webkit(self):
        """Browser session with WebKit type."""
        session = BrowserSession(
            session_id="wk-session",
            user_id="user-123",
            browser_type="webkit"
        )
        assert session.browser_type == "webkit"

    # Test: BrowserSession.start() with mocking
    @patch('tools.browser_tool.async_playwright')
    @pytest.mark.asyncio
    async def test_browser_session_start_chromium_success(self, mock_playwright_fn, browser_session):
        """Successfully start Chromium browser session."""
        # Setup mocks
        mock_pw = MagicMock()
        mock_browser = MagicMock()
        mock_context = MagicMock()
        mock_page = MagicMock()

        mock_playwright_instance = MagicMock()
        mock_playwright_instance.chromium.launch.return_value = mock_browser
        mock_playwright_instance.firefox.launch.return_value = mock_browser
        mock_playwright_instance.webkit.launch.return_value = mock_browser

        mock_browser.new_context.return_value = mock_context
        mock_context.new_page.return_value = mock_page

        mock_playwright_fn.return_value = mock_playwright_instance

        # Execute
        result = await browser_session.start()

        # Verify
        assert result == True
        assert browser_session.playwright == mock_playwright_instance
        assert browser_session.browser == mock_browser
        assert browser_session.context == mock_context
        assert browser_session.page == mock_page

        mock_browser.new_context.assert_called_once()
        mock_context.new_page.assert_called_once()

    @patch('tools.browser_tool.async_playwright')
    @pytest.mark.asyncio
    async def test_browser_session_start_firefox_success(self, mock_playwright_fn):
        """Successfully start Firefox browser session."""
        session = BrowserSession(
            session_id="ff-session",
            user_id="user-123",
            browser_type="firefox"
        )

        mock_pw = MagicMock()
        mock_browser = MagicMock()
        mock_context = MagicMock()
        mock_page = MagicMock()

        mock_playwright_instance = MagicMock()
        mock_playwright_instance.firefox.launch.return_value = mock_browser
        mock_browser.new_context.return_value = mock_context
        mock_context.new_page.return_value = mock_page

        mock_playwright_fn.return_value.return_value = mock_playwright_instance

        result = await session.start()
        assert result == True

    # Test: BrowserSession.close()
    @pytest.mark.asyncio
    async def test_browser_session_close_success(self, browser_session):
        """Successfully close browser session."""
        # Setup mocks
        browser_session.browser = MagicMock()
        browser_session.context = MagicMock()
        browser_session.page = MagicMock()
        browser_session.playwright = MagicMock()

        browser_session.page.close = AsyncMock()
        browser_session.context.close = AsyncMock()
        browser_session.browser.close = AsyncMock()
        browser_session.playwright.stop = AsyncMock()

        # Execute
        await browser_session.close()

        # Verify
        browser_session.page.close.assert_called_once()
        browser_session.context.close.assert_called_once()
        browser_session.browser.close.assert_called_once()
        browser_session.playwright.stop.assert_called_once()

    @pytest.mark.asyncio
    async def test_browser_session_close_with_none_components(self, browser_session):
        """Close browser session when components are None."""
        # All components are None initially
        await browser_session.close()
        # Should not raise exception


class TestBrowserToolCoverage:
    """Coverage expansion for BrowserTool class."""

    @pytest.fixture
    def mock_db_session(self):
        """Get mock database session."""
        return MagicMock()

    @pytest.fixture
    def browser_tool(self, mock_db_session):
        """Get browser tool instance."""
        return BrowserTool(mock_db_session)

    # Test: BrowserTool initialization
    def test_browser_tool_init(self, browser_tool):
        """Browser tool initializes correctly."""
        assert browser_tool.db == browser_tool.db
        assert browser_tool.sessions == {}

    # Test: Browser session creation
    @patch('tools.browser_tool.BrowserSession')
    @pytest.mark.asyncio
    async def test_create_browser_session_success(self, mock_session_class, browser_tool):
        """Successfully create browser session."""
        mock_session = MagicMock()
        mock_session.start = AsyncMock(return_value=True)
        mock_session_class.return_value = mock_session

        result = await browser_tool.create_session(
            user_id="user-123",
            agent_id="agent-123",
            headless=True,
            browser_type="chromium"
        )

        assert "session_id" in result
        assert result["success"] == True
        mock_session.start.assert_called_once()

    # Test: Browser navigation
    @pytest.mark.asyncio
    async def test_navigate_to_url_success(self, browser_tool):
        """Successfully navigate to URL."""
        mock_session = MagicMock()
        mock_session.page = MagicMock()
        mock_session.page.goto = AsyncMock()
        browser_tool.sessions["test-session"] = mock_session

        result = await browser_tool.navigate(
            session_id="test-session",
            url="https://example.com"
        )

        assert result["success"] == True
        mock_session.page.goto.assert_called_once_with("https://example.com", wait_until="domcontentloaded", timeout=30000)

    @pytest.mark.asyncio
    async def test_navigate_session_not_found(self, browser_tool):
        """Navigate with non-existent session."""
        result = await browser_tool.navigate(
            session_id="nonexistent",
            url="https://example.com"
        )

        assert result["success"] == False
        assert "not found" in result["error"].lower()

    # Test: Screenshot capture
    @pytest.mark.asyncio
    async def test_take_screenshot_success(self, browser_tool):
        """Successfully take screenshot."""
        mock_session = MagicMock()
        mock_session.page = MagicMock()
        mock_session.page.screenshot = AsyncMock(return_value=b"fake_image_data")
        browser_tool.sessions["test-session"] = mock_session

        result = await browser_tool.screenshot(session_id="test-session")

        assert result["success"] == True
        assert "screenshot" in result
        mock_session.page.screenshot.assert_called_once()

    # Test: Form filling
    @pytest.mark.asyncio
    async def test_fill_form_success(self, browser_tool):
        """Successfully fill form."""
        mock_session = MagicMock()
        mock_session.page = MagicMock()
        mock_session.page.fill = AsyncMock()
        browser_tool.sessions["test-session"] = mock_session

        result = await browser_tool.fill_form(
            session_id="test-session",
            selectors={"#name": "John Doe", "#email": "john@example.com"}
        )

        assert result["success"] == True
        assert mock_session.page.fill.call_count == 2

    # Test: Web scraping
    @pytest.mark.asyncio
    async def test_scrape_text_success(self, browser_tool):
        """Successfully scrape text content."""
        mock_session = MagicMock()
        mock_session.page = MagicMock()
        mock_session.page.inner_text = AsyncMock(return_value="Scraped content")
        browser_tool.sessions["test-session"] = mock_session

        result = await browser_tool.scrape(
            session_id="test-session",
            selector="body"
        )

        assert result["success"] == True
        assert result["content"] == "Scraped content"
        mock_session.page.inner_text.assert_called_once()

    @pytest.mark.asyncio
    async def test_scrape_attributes_success(self, browser_tool):
        """Successfully scrape element attributes."""
        mock_session = MagicMock()
        mock_session.page = MagicMock()
        mock_session.page.get_attribute = AsyncMock(return_value="attribute_value")
        browser_tool.sessions["test-session"] = mock_session

        result = await browser_tool.scrape(
            session_id="test-session",
            selector="a.link",
            attribute="href"
        )

        assert result["success"] == True
        assert result["content"] == "attribute_value"

    # Test: JavaScript execution
    @pytest.mark.asyncio
    async def test_execute_javascript_success(self, browser_tool):
        """Successfully execute JavaScript."""
        mock_session = MagicMock()
        mock_session.page = MagicMock()
        mock_session.page.evaluate = AsyncMock(return_value="eval result")
        browser_tool.sessions["test-session"] = mock_session

        result = await browser_tool.execute_javascript(
            session_id="test-session",
            code="document.title"
        )

        assert result["success"] == True
        assert result["result"] == "eval result"

    # Test: Session cleanup
    @pytest.mark.asyncio
    async def test_close_session_success(self, browser_tool):
        """Successfully close browser session."""
        mock_session = MagicMock()
        mock_session.close = AsyncMock()
        browser_tool.sessions["test-session"] = mock_session

        result = await browser_tool.close_session(session_id="test-session")

        assert result["success"] == True
        assert "test-session" not in browser_tool.sessions
        mock_session.close.assert_called_once()

    @pytest.mark.asyncio
    async def test_close_session_not_found(self, browser_tool):
        """Close non-existent session."""
        result = await browser_tool.close_session(session_id="nonexistent")
        assert result["success"] == False


class TestBrowserToolErrorHandling:
    """Coverage expansion for browser tool error handling."""

    @pytest.fixture
    def mock_db_session(self):
        """Get mock database session."""
        return MagicMock()

    @pytest.fixture
    def browser_tool(self, mock_db_session):
        """Get browser tool instance."""
        return BrowserTool(mock_db_session)

    # Test: Invalid URL handling
    @pytest.mark.asyncio
    async def test_navigate_invalid_url(self, browser_tool):
        """Navigate with invalid URL."""
        mock_session = MagicMock()
        mock_session.page = MagicMock()
        mock_session.page.goto = AsyncMock(side_effect=Exception("Invalid URL"))
        browser_tool.sessions["test-session"] = mock_session

        result = await browser_tool.navigate(
            session_id="test-session",
            url="not-a-url"
        )

        assert result["success"] == False

    # Test: Selector not found
    @pytest.mark.asyncio
    async def test_fill_form_selector_not_found(self, browser_tool):
        """Fill form with selector that doesn't exist."""
        mock_session = MagicMock()
        mock_session.page = MagicMock()
        mock_session.page.fill = AsyncMock(side_effect=Exception("Element not found"))
        browser_tool.sessions["test-session"] = mock_session

        result = await browser_tool.fill_form(
            session_id="test-session",
            selectors={"#nonexistent": "value"}
        )

        assert result["success"] == False

    # Test: JavaScript execution error
    @pytest.mark.asyncio
    async def test_execute_javascript_error(self, browser_tool):
        """Execute JavaScript that raises error."""
        mock_session = MagicMock()
        mock_session.page = MagicMock()
        mock_session.page.evaluate = AsyncMock(side_effect=Exception("Syntax error"))
        browser_tool.sessions["test-session"] = mock_session

        result = await browser_tool.execute_javascript(
            session_id="test-session",
            code="invalid javascript"
        )

        assert result["success"] == False