mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-14 09:39:11 +02:00
* Use anyio as testing backend * Remove asyncio markers * Update streamable http tests * Replace all subprocess tests * Replace anyio task groups with asyncio context managers in tests - Convert run_server_async from anyio task group pattern to asyncio.create_task with async context manager - Remove task_group fixture from conftest - Update all test fixtures to use async with run_server_async pattern - Remove TaskGroup imports from all test files - Tests now work with pytest-asyncio instead of pytest-anyio * Update test_github_provider_integration.py
201 lines
7.4 KiB
Python
201 lines
7.4 KiB
Python
"""Tests for WorkOS OAuth provider."""
|
|
|
|
import os
|
|
from unittest.mock import patch
|
|
from urllib.parse import urlparse
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from fastmcp import Client, FastMCP
|
|
from fastmcp.client.transports import StreamableHttpTransport
|
|
from fastmcp.server.auth.providers.workos import AuthKitProvider, WorkOSProvider
|
|
from fastmcp.utilities.tests import HeadlessOAuth, run_server_async
|
|
|
|
|
|
class TestWorkOSProvider:
|
|
"""Test WorkOS OAuth provider functionality."""
|
|
|
|
def test_init_with_explicit_params(self):
|
|
"""Test WorkOSProvider initialization with explicit parameters."""
|
|
provider = WorkOSProvider(
|
|
client_id="client_test123",
|
|
client_secret="secret_test456",
|
|
authkit_domain="https://test.authkit.app",
|
|
base_url="https://myserver.com",
|
|
required_scopes=["openid", "profile"],
|
|
)
|
|
|
|
assert provider._upstream_client_id == "client_test123"
|
|
assert provider._upstream_client_secret.get_secret_value() == "secret_test456"
|
|
assert str(provider.base_url) == "https://myserver.com/"
|
|
|
|
@pytest.mark.parametrize(
|
|
"scopes_env",
|
|
[
|
|
"openid,email",
|
|
'["openid", "email"]',
|
|
],
|
|
)
|
|
def test_init_with_env_vars(self, scopes_env):
|
|
"""Test WorkOSProvider initialization from environment variables."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"FASTMCP_SERVER_AUTH_WORKOS_CLIENT_ID": "env_client",
|
|
"FASTMCP_SERVER_AUTH_WORKOS_CLIENT_SECRET": "env_secret",
|
|
"FASTMCP_SERVER_AUTH_WORKOS_AUTHKIT_DOMAIN": "https://env.authkit.app",
|
|
"FASTMCP_SERVER_AUTH_WORKOS_BASE_URL": "https://envserver.com",
|
|
"FASTMCP_SERVER_AUTH_WORKOS_REQUIRED_SCOPES": scopes_env,
|
|
},
|
|
):
|
|
provider = WorkOSProvider()
|
|
|
|
assert provider._upstream_client_id == "env_client"
|
|
assert provider._upstream_client_secret.get_secret_value() == "env_secret"
|
|
assert str(provider.base_url) == "https://envserver.com/"
|
|
assert provider._token_validator.required_scopes == [
|
|
"openid",
|
|
"email",
|
|
]
|
|
|
|
def test_init_missing_client_id_raises_error(self):
|
|
"""Test that missing client_id raises ValueError."""
|
|
with pytest.raises(ValueError, match="client_id is required"):
|
|
WorkOSProvider(
|
|
client_secret="test_secret",
|
|
authkit_domain="https://test.authkit.app",
|
|
)
|
|
|
|
def test_init_missing_client_secret_raises_error(self):
|
|
"""Test that missing client_secret raises ValueError."""
|
|
with pytest.raises(ValueError, match="client_secret is required"):
|
|
WorkOSProvider(
|
|
client_id="test_client",
|
|
authkit_domain="https://test.authkit.app",
|
|
)
|
|
|
|
def test_init_missing_authkit_domain_raises_error(self):
|
|
"""Test that missing authkit_domain raises ValueError."""
|
|
with pytest.raises(ValueError, match="authkit_domain is required"):
|
|
WorkOSProvider(
|
|
client_id="test_client",
|
|
client_secret="test_secret",
|
|
)
|
|
|
|
def test_authkit_domain_https_prefix_handling(self):
|
|
"""Test that authkit_domain handles missing https:// prefix."""
|
|
# Without https:// - should add it
|
|
provider1 = WorkOSProvider(
|
|
client_id="test_client",
|
|
client_secret="test_secret",
|
|
authkit_domain="test.authkit.app",
|
|
base_url="https://myserver.com",
|
|
)
|
|
parsed = urlparse(provider1._upstream_authorization_endpoint)
|
|
assert parsed.scheme == "https"
|
|
assert parsed.netloc == "test.authkit.app"
|
|
assert parsed.path == "/oauth2/authorize"
|
|
|
|
# With https:// - should keep it
|
|
provider2 = WorkOSProvider(
|
|
client_id="test_client",
|
|
client_secret="test_secret",
|
|
authkit_domain="https://test.authkit.app",
|
|
base_url="https://myserver.com",
|
|
)
|
|
parsed = urlparse(provider2._upstream_authorization_endpoint)
|
|
assert parsed.scheme == "https"
|
|
assert parsed.netloc == "test.authkit.app"
|
|
assert parsed.path == "/oauth2/authorize"
|
|
|
|
# With http:// - should be preserved
|
|
provider3 = WorkOSProvider(
|
|
client_id="test_client",
|
|
client_secret="test_secret",
|
|
authkit_domain="http://localhost:8080",
|
|
base_url="https://myserver.com",
|
|
)
|
|
parsed = urlparse(provider3._upstream_authorization_endpoint)
|
|
assert parsed.scheme == "http"
|
|
assert parsed.netloc == "localhost:8080"
|
|
assert parsed.path == "/oauth2/authorize"
|
|
|
|
def test_init_defaults(self):
|
|
"""Test that default values are applied correctly."""
|
|
provider = WorkOSProvider(
|
|
client_id="test_client",
|
|
client_secret="test_secret",
|
|
authkit_domain="https://test.authkit.app",
|
|
)
|
|
|
|
# Check defaults
|
|
assert provider.base_url is None
|
|
assert provider._redirect_path == "/auth/callback"
|
|
# WorkOS provider has no default scopes but we can't easily verify without accessing internals
|
|
|
|
def test_oauth_endpoints_configured_correctly(self):
|
|
"""Test that OAuth endpoints are configured correctly."""
|
|
provider = WorkOSProvider(
|
|
client_id="test_client",
|
|
client_secret="test_secret",
|
|
authkit_domain="https://test.authkit.app",
|
|
base_url="https://myserver.com",
|
|
)
|
|
|
|
# Check that endpoints use the authkit domain
|
|
assert (
|
|
provider._upstream_authorization_endpoint
|
|
== "https://test.authkit.app/oauth2/authorize"
|
|
)
|
|
assert (
|
|
provider._upstream_token_endpoint == "https://test.authkit.app/oauth2/token"
|
|
)
|
|
assert (
|
|
provider._upstream_revocation_endpoint is None
|
|
) # WorkOS doesn't support revocation
|
|
|
|
|
|
@pytest.fixture
|
|
async def mcp_server_url():
|
|
"""Start AuthKit server."""
|
|
mcp = FastMCP(
|
|
auth=AuthKitProvider(
|
|
authkit_domain="https://respectful-lullaby-34-staging.authkit.app",
|
|
base_url="http://localhost:4321",
|
|
)
|
|
)
|
|
|
|
@mcp.tool
|
|
def add(a: int, b: int) -> int:
|
|
return a + b
|
|
|
|
async with run_server_async(mcp, transport="http") as url:
|
|
yield url
|
|
|
|
|
|
@pytest.fixture
|
|
def client_with_headless_oauth(mcp_server_url: str) -> Client:
|
|
"""Client with headless OAuth that bypasses browser interaction."""
|
|
return Client(
|
|
transport=StreamableHttpTransport(mcp_server_url),
|
|
auth=HeadlessOAuth(mcp_url=mcp_server_url),
|
|
)
|
|
|
|
|
|
class TestAuthKitProvider:
|
|
async def test_unauthorized_access(self, mcp_server_url: str):
|
|
with pytest.raises(httpx.HTTPStatusError) as exc_info:
|
|
async with Client(mcp_server_url) as client:
|
|
tools = await client.list_tools() # noqa: F841
|
|
|
|
assert isinstance(exc_info.value, httpx.HTTPStatusError)
|
|
assert exc_info.value.response.status_code == 401
|
|
assert "tools" not in locals()
|
|
|
|
# async def test_authorized_access(self, client_with_headless_oauth: Client):
|
|
# async with client_with_headless_oauth:
|
|
# tools = await client_with_headless_oauth.list_tools()
|
|
# assert tools is not None
|
|
# assert len(tools) > 0
|
|
# assert "add" in tools
|