"""Tests for OAuth proxy UI and error page rendering.""" from unittest.mock import Mock from key_value.aio.stores.memory import MemoryStore from starlette.requests import Request from starlette.responses import HTMLResponse from fastmcp.server.auth.oauth_proxy import OAuthProxy from fastmcp.server.auth.oauth_proxy.ui import create_consent_html, create_error_html from fastmcp.server.auth.providers.jwt import JWTVerifier class TestErrorPageRendering: """Test error page rendering for OAuth callback errors.""" def test_create_error_html_basic(self): """Test basic error page generation.""" html = create_error_html( error_title="Test Error", error_message="This is a test error message", ) # Verify it's valid HTML assert "" in html assert "Test Error" in html assert "This is a test error message" in html assert 'class="info-box error"' in html def test_create_error_html_with_details(self): """Test error page with error details.""" html = create_error_html( error_title="OAuth Error", error_message="Authentication failed", error_details={ "Error Code": "invalid_scope", "Description": "Requested scope does not exist", }, ) # Verify error details are included assert "Error Details" in html assert "Error Code" in html assert "invalid_scope" in html assert "Description" in html assert "Requested scope does not exist" in html def test_create_error_html_escapes_user_input(self): """Test that error page properly escapes HTML in user input.""" html = create_error_html( error_title="Error ", error_message="Message with HTML tags", error_details={"Key" not in html assert "<script>" in html assert "HTML" not in html assert "<b>HTML</b>" in html async def test_callback_error_returns_html_page(self): """Test that OAuth callback errors return styled HTML instead of data: URLs.""" # Create a minimal OAuth proxy provider = OAuthProxy( upstream_authorization_endpoint="https://idp.example.com/authorize", upstream_token_endpoint="https://idp.example.com/token", upstream_client_id="test-client", upstream_client_secret="test-secret", token_verifier=JWTVerifier( jwks_uri="https://idp.example.com/.well-known/jwks.json", issuer="https://idp.example.com", audience="test-client", ), base_url="http://localhost:8000", jwt_signing_key="test-signing-key", client_storage=MemoryStore(), ) # Mock a request with an error from the IdP mock_request = Mock(spec=Request) mock_request.query_params = { "error": "invalid_scope", "error_description": "The application asked for scope 'read' that doesn't exist", "state": "test-state", } # Call the callback handler response = await provider._handle_idp_callback(mock_request) # Verify we get an HTMLResponse, not a RedirectResponse assert isinstance(response, HTMLResponse) assert response.status_code == 400 # Verify the response contains the error message assert b"invalid_scope" in response.body assert b"doesn't exist" in response.body # HTML-escaped apostrophe assert b"OAuth Error" in response.body class TestConsentPageRendering: """Test consent page rendering and escaping.""" def test_create_consent_html_escapes_client_id_in_details(self): """Test that Application ID is escaped in advanced details.""" html = create_consent_html( client_id='evil', redirect_uri="https://example.com/callback", scopes=["read"], txn_id="txn", csrf_token="csrf", ) assert 'evil' not in html assert "evil<img src=x onerror=alert("xss")>" in html