| """Tests for Maris browser automation endpoints.""" |
|
|
| from __future__ import annotations |
|
|
| import base64 |
|
|
| import pytest |
| from fastapi import HTTPException |
|
|
| from maris_core.browser.automation import ( |
| BrowserAutomationUnavailableError, |
| BrowserExtractRequest, |
| BrowserNavigateRequest, |
| BrowserScreenshotRequest, |
| BrowserSessionRequest, |
| BrowserSessionStartRequest, |
| _browser_sessions, |
| browser_capabilities, |
| close_browser_session, |
| extract_browser_text, |
| navigate_browser, |
| screenshot_browser, |
| start_browser_session, |
| ) |
|
|
|
|
| class _FakeBrowserSession: |
| def __init__(self) -> None: |
| self.headless = True |
| self.viewport = {"width": 1280, "height": 720} |
| self.url = "about:blank" |
| self.title = "Blank" |
| self.closed = False |
|
|
| async def snapshot(self) -> dict[str, str]: |
| return {"url": self.url, "title": self.title} |
|
|
| async def navigate(self, url: str, *, wait_until: str, timeout_ms: int) -> dict[str, str]: |
| assert wait_until == "load" |
| assert timeout_ms == 3210 |
| self.url = url |
| self.title = "Target page" |
| return await self.snapshot() |
|
|
| async def click( |
| self, selector: str, *, timeout_ms: int, wait_until_after: str | None = None |
| ) -> dict[str, str]: |
| raise AssertionError(f"Unexpected click on {selector} with {timeout_ms} {wait_until_after}") |
|
|
| async def fill( |
| self, selector: str, value: str, *, timeout_ms: int, submit: bool |
| ) -> dict[str, str]: |
| raise AssertionError(f"Unexpected fill on {selector}={value} with {timeout_ms} {submit}") |
|
|
| async def extract_text(self, selector: str | None, *, timeout_ms: int, max_length: int) -> str: |
| assert selector == "#main" |
| assert timeout_ms == 4321 |
| return "Sveiks no browser workflow"[:max_length] |
|
|
| async def screenshot_png(self, *, full_page: bool) -> bytes: |
| assert full_page is True |
| return b"png-bytes" |
|
|
| async def close(self) -> None: |
| self.closed = True |
|
|
|
|
| @pytest.fixture(autouse=True) |
| def clear_browser_sessions() -> None: |
| _browser_sessions.clear() |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_browser_capabilities_are_exposed() -> None: |
| response = await browser_capabilities() |
|
|
| assert response.provider == "playwright" |
| assert "navigate" in response.supported_actions |
| assert response.max_sessions >= 1 |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_browser_session_lifecycle_supports_navigation_extract_and_screenshot( |
| monkeypatch, |
| ) -> None: |
| fake_session = _FakeBrowserSession() |
|
|
| async def _fake_create_browser_session( |
| *, headless: bool, viewport: dict[str, int] |
| ) -> _FakeBrowserSession: |
| assert headless is True |
| assert viewport == {"width": 1280, "height": 720} |
| return fake_session |
|
|
| monkeypatch.setattr( |
| "maris_core.browser.automation._create_browser_session", |
| _fake_create_browser_session, |
| ) |
|
|
| started = await start_browser_session( |
| BrowserSessionStartRequest(headless=True, viewport_width=1280, viewport_height=720) |
| ) |
| session_id = started.session_id |
|
|
| navigated = await navigate_browser( |
| BrowserNavigateRequest( |
| session_id=session_id, |
| url="https://example.com", |
| wait_until="load", |
| timeout_ms=3210, |
| ) |
| ) |
| extracted = await extract_browser_text( |
| BrowserExtractRequest( |
| session_id=session_id, |
| selector="#main", |
| timeout_ms=4321, |
| max_length=128, |
| ) |
| ) |
| screenshot = await screenshot_browser( |
| BrowserScreenshotRequest(session_id=session_id, full_page=True) |
| ) |
| closed = await close_browser_session(BrowserSessionRequest(session_id=session_id)) |
|
|
| assert started.active is True |
| assert navigated.url == "https://example.com" |
| assert extracted.text == "Sveiks no browser workflow" |
| assert base64.b64decode(screenshot.image_base64) == b"png-bytes" |
| assert closed.active is False |
| assert fake_session.closed is True |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_browser_start_returns_503_when_playwright_is_unavailable(monkeypatch) -> None: |
| async def _unavailable(**_: object) -> None: |
| raise BrowserAutomationUnavailableError("Playwright nav pieejams") |
|
|
| monkeypatch.setattr("maris_core.browser.automation._create_browser_session", _unavailable) |
|
|
| with pytest.raises(HTTPException) as exc_info: |
| await start_browser_session(BrowserSessionStartRequest()) |
|
|
| assert exc_info.value.status_code == 503 |
| assert "Playwright" in exc_info.value.detail |
|
|
|
|
| @pytest.mark.parametrize( |
| "url", |
| [ |
| "file:///etc/passwd", |
| "javascript:alert(1)", |
| "ftp://example.com", |
| "chrome://settings", |
| ], |
| ) |
| def test_browser_request_rejects_unsafe_schemes(url: str) -> None: |
| with pytest.raises(ValueError): |
| BrowserNavigateRequest(session_id="session", url=url) |
|
|