| """Browser API contract tests using Schemathesis for OpenAPI compliance. |
| |
| Validates that browser endpoints (session, navigation, interaction) conform to |
| their OpenAPI specification. Browser endpoints handle Playwright-based browser |
| automation for web scraping, form filling, and screenshots. |
| |
| Contract test coverage: |
| - POST /api/browser/session - Create browser session |
| - GET /api/browser/sessions - List browser sessions |
| - DELETE /api/browser/session/{id} - Close browser session |
| - POST /api/browser/navigate - Navigate to URL |
| - POST /api/browser/click - Click element |
| - POST /api/browser/fill - Fill form field |
| - POST /api/browser/screenshot - Take screenshot |
| """ |
| import pytest |
| from fastapi.testclient import TestClient |
| from main_api_app import app |
| from tests.contract.conftest import schema |
| from unittest.mock import MagicMock, patch |
|
|
|
|
| class TestBrowserSessionContract: |
| """Contract tests for browser session management endpoints.""" |
|
|
| def test_create_session_contracts(self): |
| """Test POST /api/browser/session validates response schema.""" |
| if "/api/browser/session" in schema: |
| operation = schema["/api/browser/session"]["POST"] |
| with TestClient(app) as client: |
| response = client.post("/api/browser/session") |
| |
| operation.validate_response(response) |
| |
| assert response.status_code in [200, 400, 401, 403, 500, 503] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_create_session_with_config(self): |
| """Test that session creation config conforms to schema.""" |
| if "/api/browser/session" in schema: |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/browser/session", |
| json={ |
| "headless": True, |
| "browser_type": "chromium" |
| } |
| ) |
| assert response.status_code in [200, 400, 401, 403, 500, 503] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_list_sessions_contracts(self): |
| """Test GET /api/browser/sessions validates response schema.""" |
| if "/api/browser/sessions" in schema: |
| operation = schema["/api/browser/sessions"]["GET"] |
| with TestClient(app) as client: |
| response = client.get("/api/browser/sessions") |
| |
| operation.validate_response(response) |
| assert response.status_code in [200, 401, 403] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_close_session_contracts(self): |
| """Test DELETE /api/browser/session/{id} validates response schema.""" |
| if "/api/browser/session/{session_id}" in schema: |
| operation = schema["/api/browser/session/{session_id}"]["DELETE"] |
| with TestClient(app) as client: |
| response = client.delete("/api/browser/session/test-session-id") |
| |
| operation.validate_response(response) |
| assert response.status_code in [200, 204, 401, 403, 404] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_session_not_found(self): |
| """Test that deleting non-existent session returns 404.""" |
| if "/api/browser/session/{session_id}" in schema: |
| with TestClient(app) as client: |
| response = client.delete("/api/browser/session/nonexistent-session-999") |
| assert response.status_code in [200, 204, 401, 403, 404] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
|
|
| class TestBrowserNavigationContract: |
| """Contract tests for browser navigation endpoints.""" |
|
|
| def test_navigate_contracts(self): |
| """Test POST /api/browser/navigate validates response schema.""" |
| if "/api/browser/navigate" in schema: |
| operation = schema["/api/browser/navigate"]["POST"] |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/browser/navigate", |
| json={ |
| "session_id": "test-session", |
| "url": "https://example.com" |
| } |
| ) |
| |
| operation.validate_response(response) |
| assert response.status_code in [200, 400, 401, 403, 422, 500] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_navigate_request_schema(self): |
| """Test that URL parameter validation conforms to schema.""" |
| if "/api/browser/navigate" in schema: |
| with TestClient(app) as client: |
| |
| response = client.post( |
| "/api/browser/navigate", |
| json={ |
| "session_id": "test-session", |
| "url": "https://example.com/page" |
| } |
| ) |
| assert response.status_code in [200, 400, 401, 403, 422, 500] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_navigate_errors(self): |
| """Test that 400/422 schemas work for invalid URLs.""" |
| if "/api/browser/navigate" in schema: |
| with TestClient(app) as client: |
| |
| response = client.post( |
| "/api/browser/navigate", |
| json={ |
| "session_id": "test-session" |
| |
| } |
| ) |
| |
| assert response.status_code in [200, 400, 401, 403, 422, 500] |
|
|
| |
| response = client.post( |
| "/api/browser/navigate", |
| json={ |
| "session_id": "test-session", |
| "url": "not-a-valid-url" |
| } |
| ) |
| assert response.status_code in [200, 400, 401, 403, 422, 500] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_navigate_with_options(self): |
| """Test that navigation options conform to schema.""" |
| if "/api/browser/navigate" in schema: |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/browser/navigate", |
| json={ |
| "session_id": "test-session", |
| "url": "https://example.com", |
| "wait_until": "networkidle", |
| "timeout": 30000 |
| } |
| ) |
| assert response.status_code in [200, 400, 401, 403, 422, 500] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
|
|
| class TestBrowserInteractionContract: |
| """Contract tests for browser interaction endpoints.""" |
|
|
| def test_click_contracts(self): |
| """Test POST /api/browser/click validates response schema.""" |
| if "/api/browser/click" in schema: |
| operation = schema["/api/browser/click"]["POST"] |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/browser/click", |
| json={ |
| "session_id": "test-session", |
| "selector": "#submit-button" |
| } |
| ) |
| |
| operation.validate_response(response) |
| assert response.status_code in [200, 400, 401, 403, 422, 500] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_fill_contracts(self): |
| """Test POST /api/browser/fill validates response schema.""" |
| if "/api/browser/fill" in schema: |
| operation = schema["/api/browser/fill"]["POST"] |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/browser/fill", |
| json={ |
| "session_id": "test-session", |
| "selector": "#username", |
| "value": "testuser" |
| } |
| ) |
| |
| operation.validate_response(response) |
| assert response.status_code in [200, 400, 401, 403, 422, 500] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_fill_multiple_fields(self): |
| """Test that filling multiple fields conforms to schema.""" |
| if "/api/browser/fill" in schema: |
| with TestClient(app) as client: |
| |
| response = client.post( |
| "/api/browser/fill", |
| json={ |
| "session_id": "test-session", |
| "fields": [ |
| {"selector": "#username", "value": "user"}, |
| {"selector": "#password", "value": "pass"} |
| ] |
| } |
| ) |
| assert response.status_code in [200, 400, 401, 403, 422, 500] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_screenshot_contracts(self): |
| """Test POST /api/browser/screenshot validates response schema.""" |
| if "/api/browser/screenshot" in schema: |
| operation = schema["/api/browser/screenshot"]["POST"] |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/browser/screenshot", |
| json={ |
| "session_id": "test-session" |
| } |
| ) |
| |
| operation.validate_response(response) |
| assert response.status_code in [200, 400, 401, 403, 500] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_screenshot_response(self): |
| """Test that screenshot response includes Base64 image.""" |
| if "/api/browser/screenshot" in schema: |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/browser/screenshot", |
| json={ |
| "session_id": "test-session", |
| "format": "png" |
| } |
| ) |
| |
| if response.status_code == 200: |
| json_resp = response.json() |
| |
| assert "image" in json_resp or "data" in json_resp |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_screenshot_options(self): |
| """Test that screenshot options conform to schema.""" |
| if "/api/browser/screenshot" in schema: |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/browser/screenshot", |
| json={ |
| "session_id": "test-session", |
| "format": "jpeg", |
| "quality": 80, |
| "full_page": True |
| } |
| ) |
| assert response.status_code in [200, 400, 401, 403, 500] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_execute_script_contracts(self): |
| """Test POST /api/browser/execute validates response schema.""" |
| if "/api/browser/execute" in schema: |
| operation = schema["/api/browser/execute"]["POST"] |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/browser/execute", |
| json={ |
| "session_id": "test-session", |
| "script": "document.title" |
| } |
| ) |
| |
| operation.validate_response(response) |
| assert response.status_code in [200, 400, 401, 403, 422, 500] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
|
|
| class TestBrowserGovernanceContract: |
| """Contract tests for browser governance and permissions.""" |
|
|
| def test_governance_headers(self): |
| """Test that X-Agent-Maturity header is processed.""" |
| with TestClient(app) as client: |
| |
| if "/api/browser/session" in schema: |
| response = client.post( |
| "/api/browser/session", |
| headers={"X-Agent-Maturity": "SUPERVISED"} |
| ) |
| |
| assert response.status_code in [200, 400, 401, 403, 500, 503] |
|
|
| def test_permission_denied(self): |
| """Test that 403 response conforms to schema.""" |
| |
| |
| if "/api/browser/session" in schema: |
| with TestClient(app) as client: |
| response = client.post("/api/browser/session") |
| |
| if response.status_code == 403: |
| |
| json_resp = response.json() |
| assert "detail" in json_resp or "error" in json_resp |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_unauthorized(self): |
| """Test that 401 response conforms to schema.""" |
| if "/api/browser/session" in schema: |
| with TestClient(app) as client: |
| response = client.post("/api/browser/session") |
| |
| if response.status_code == 401: |
| |
| json_resp = response.json() |
| assert "detail" in json_resp or "error" in json_resp |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
|
|
| class TestBrowserErrorHandlingContract: |
| """Contract tests for browser error handling.""" |
|
|
| def test_session_timeout(self): |
| """Test that session timeout returns appropriate error.""" |
| if "/api/browser/navigate" in schema: |
| with TestClient(app) as client: |
| |
| response = client.post( |
| "/api/browser/navigate", |
| json={ |
| "session_id": "expired-session", |
| "url": "https://example.com" |
| } |
| ) |
| |
| assert response.status_code in [200, 400, 401, 403, 404, 500] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_element_not_found(self): |
| """Test that missing element returns appropriate error.""" |
| if "/api/browser/click" in schema: |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/browser/click", |
| json={ |
| "session_id": "test-session", |
| "selector": "#nonexistent-element" |
| } |
| ) |
| |
| assert response.status_code in [200, 400, 401, 403, 404, 500] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_navigation_timeout(self): |
| """Test that navigation timeout returns appropriate error.""" |
| if "/api/browser/navigate" in schema: |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/browser/navigate", |
| json={ |
| "session_id": "test-session", |
| "url": "https://example.com", |
| "timeout": 1 |
| } |
| ) |
| |
| assert response.status_code in [200, 400, 401, 403, 500] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
|
|
| class TestBrowserCDPContract: |
| """Contract tests for Chrome DevTools Protocol (CDP) endpoints.""" |
|
|
| def test_cdp_session_contracts(self): |
| """Test CDP session creation validates schema.""" |
| if "/api/browser/cdp" in schema: |
| operation = schema["/api/browser/cdp"]["POST"] |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/browser/cdp", |
| json={ |
| "session_id": "test-session" |
| } |
| ) |
| |
| operation.validate_response(response) |
| assert response.status_code in [200, 400, 401, 403, 500] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_cdp_execute_contracts(self): |
| """Test CDP command execution validates schema.""" |
| |
| |
| if "/api/browser/cdp/execute" in schema: |
| operation = schema["/api/browser/cdp/execute"]["POST"] |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/browser/cdp/execute", |
| json={ |
| "session_id": "test-session", |
| "command": "Page.navigate", |
| "params": {"url": "https://example.com"} |
| } |
| ) |
| |
| operation.validate_response(response) |
| assert response.status_code in [200, 400, 401, 403, 422, 500] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
|
|
| class TestBrowserInputStrategies: |
| """Hypothesis strategies for browser-specific input generation.""" |
|
|
| def test_valid_url_strategy(self): |
| """Test that URL strategy generates valid URLs.""" |
| |
| |
| valid_urls = [ |
| "https://example.com", |
| "https://example.com/page", |
| "https://example.com/page?query=value", |
| "https://example.com:8080/page" |
| ] |
| |
| for url in valid_urls: |
| |
| assert url.startswith(("http://", "https://")) |
|
|
| def test_selector_strategy(self): |
| """Test that selector strategy generates valid CSS selectors.""" |
| |
| valid_selectors = [ |
| "#id", |
| ".class", |
| "tag", |
| "#id .class", |
| "tag[attr=value]", |
| "parent > child" |
| ] |
| |
| for selector in valid_selectors: |
| |
| assert len(selector) > 0 |
|
|