"""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") # Validate response against OpenAPI schema operation.validate_response(response) # May return 200, 400, 401, 403, or 500 if Playwright not available 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") # Validate response against OpenAPI schema 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") # Validate response against OpenAPI schema 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" } ) # Validate response against OpenAPI schema 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: # Test with valid URL 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: # Test with invalid URL (missing required field) response = client.post( "/api/browser/navigate", json={ "session_id": "test-session" # Missing url field } ) # Should return 400 or 422 assert response.status_code in [200, 400, 401, 403, 422, 500] # Test with malformed URL 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" } ) # Validate response against OpenAPI schema 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" } ) # Validate response against OpenAPI schema 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: # Test with fill array (if supported) 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" } ) # Validate response against OpenAPI schema 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 successful, should have image data if response.status_code == 200: json_resp = response.json() # Should have base64 image data 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" } ) # Validate response against OpenAPI schema 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: # Test with governance header if "/api/browser/session" in schema: response = client.post( "/api/browser/session", headers={"X-Agent-Maturity": "SUPERVISED"} ) # Header may or may not be enforced in contract tests assert response.status_code in [200, 400, 401, 403, 500, 503] def test_permission_denied(self): """Test that 403 response conforms to schema.""" # Browser operations require INTERN+ maturity # This test validates the 403 response schema if "/api/browser/session" in schema: with TestClient(app) as client: response = client.post("/api/browser/session") # If permission denied, should return 403 if response.status_code == 403: # Validate error response structure 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 unauthorized, should return 401 if response.status_code == 401: # Validate error response structure 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: # Test with expired session response = client.post( "/api/browser/navigate", json={ "session_id": "expired-session", "url": "https://example.com" } ) # Should return error for expired session 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" } ) # Should return error if element not found 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 # 1ms timeout (will fail) } ) # Should return error for timeout 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" } ) # Validate response against OpenAPI schema 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.""" # CDP allows sending arbitrary Chrome DevTools Protocol commands # Schema validation is more complex due to dynamic command structure 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"} } ) # Validate response against OpenAPI schema 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.""" # This documents the URL validation strategy for Hypothesis # Valid URLs should be absolute and well-formed valid_urls = [ "https://example.com", "https://example.com/page", "https://example.com/page?query=value", "https://example.com:8080/page" ] # These should all pass URL validation for url in valid_urls: # In actual implementation, would use hypothesis.strategies assert url.startswith(("http://", "https://")) def test_selector_strategy(self): """Test that selector strategy generates valid CSS selectors.""" # This documents the CSS selector validation strategy valid_selectors = [ "#id", ".class", "tag", "#id .class", "tag[attr=value]", "parent > child" ] # These should all be valid CSS selectors for selector in valid_selectors: # In actual implementation, would use hypothesis.strategies assert len(selector) > 0