fix: Separate OAuth fixtures for opaque vs JWT tokens
Previous fix created a JWT OAuth client for all tests, which broke the regular OAuth server (port 8001) that expects opaque tokens. This commit: 1. Reverts shared_oauth_client_credentials to use regular OAuth (opaque tokens) 2. Creates new shared_jwt_oauth_client_credentials for JWT OAuth clients 3. Creates new playwright_oauth_token_jwt fixture using JWT credentials 4. Updates nc_mcp_oauth_jwt_client to use JWT token fixture This ensures: - Regular OAuth tests (port 8001) use opaque tokens - JWT OAuth tests (port 8002) use JWT tokens with embedded scopes Fixes remaining CI failure in test_mcp_oauth_server_connection
This commit is contained in:
+106
-8
@@ -187,7 +187,7 @@ async def nc_mcp_oauth_client(
|
|||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
async def nc_mcp_oauth_jwt_client(
|
async def nc_mcp_oauth_jwt_client(
|
||||||
anyio_backend,
|
anyio_backend,
|
||||||
playwright_oauth_token: str,
|
playwright_oauth_token_jwt: str,
|
||||||
) -> AsyncGenerator[ClientSession, Any]:
|
) -> AsyncGenerator[ClientSession, Any]:
|
||||||
"""
|
"""
|
||||||
Fixture to create an MCP client session for JWT OAuth integration tests.
|
Fixture to create an MCP client session for JWT OAuth integration tests.
|
||||||
@@ -203,7 +203,7 @@ async def nc_mcp_oauth_jwt_client(
|
|||||||
"""
|
"""
|
||||||
async for session in create_mcp_client_session(
|
async for session in create_mcp_client_session(
|
||||||
url="http://127.0.0.1:8002/mcp",
|
url="http://127.0.0.1:8002/mcp",
|
||||||
token=playwright_oauth_token,
|
token=playwright_oauth_token_jwt,
|
||||||
client_name="OAuth JWT MCP (Playwright)",
|
client_name="OAuth JWT MCP (Playwright)",
|
||||||
):
|
):
|
||||||
yield session
|
yield session
|
||||||
@@ -883,14 +883,16 @@ async def shared_oauth_client_credentials(anyio_backend, oauth_callback_server):
|
|||||||
"""
|
"""
|
||||||
Fixture to obtain shared OAuth client credentials that will be reused for all users.
|
Fixture to obtain shared OAuth client credentials that will be reused for all users.
|
||||||
|
|
||||||
Creates a JWT OAuth client with full scopes (nc:read and nc:write) to match the MCP server's
|
This registers a single OAuth client with Nextcloud that matches the MCP server's
|
||||||
behavior. This allows all test users to authenticate using the same client_id/secret.
|
registration, allowing all test users to authenticate using the same client_id/secret.
|
||||||
|
|
||||||
Now uses the real OAuth callback server for reliable token acquisition.
|
Uses regular (opaque token) OAuth client for the standard OAuth MCP server (port 8001).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (client_id, client_secret, callback_url, token_endpoint, authorization_endpoint)
|
Tuple of (client_id, client_secret, callback_url, token_endpoint, authorization_endpoint)
|
||||||
"""
|
"""
|
||||||
|
from nextcloud_mcp_server.auth.client_registration import load_or_register_client
|
||||||
|
|
||||||
nextcloud_host = os.getenv("NEXTCLOUD_HOST")
|
nextcloud_host = os.getenv("NEXTCLOUD_HOST")
|
||||||
if not nextcloud_host:
|
if not nextcloud_host:
|
||||||
pytest.skip("Shared OAuth client requires NEXTCLOUD_HOST")
|
pytest.skip("Shared OAuth client requires NEXTCLOUD_HOST")
|
||||||
@@ -901,6 +903,82 @@ async def shared_oauth_client_credentials(anyio_backend, oauth_callback_server):
|
|||||||
logger.info("Setting up shared OAuth client credentials for all test users...")
|
logger.info("Setting up shared OAuth client credentials for all test users...")
|
||||||
logger.info(f"Using real callback server at: {callback_url}")
|
logger.info(f"Using real callback server at: {callback_url}")
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as http_client:
|
||||||
|
# OIDC Discovery
|
||||||
|
discovery_url = f"{nextcloud_host}/.well-known/openid-configuration"
|
||||||
|
discovery_response = await http_client.get(discovery_url)
|
||||||
|
discovery_response.raise_for_status()
|
||||||
|
oidc_config = discovery_response.json()
|
||||||
|
|
||||||
|
token_endpoint = oidc_config.get("token_endpoint")
|
||||||
|
registration_endpoint = oidc_config.get("registration_endpoint")
|
||||||
|
authorization_endpoint = oidc_config.get("authorization_endpoint")
|
||||||
|
|
||||||
|
if not token_endpoint or not authorization_endpoint:
|
||||||
|
raise ValueError(
|
||||||
|
"OIDC discovery missing required endpoints (token_endpoint or authorization_endpoint)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try to load existing client first
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from nextcloud_mcp_server.auth.client_registration import load_client_from_file
|
||||||
|
|
||||||
|
storage_path = Path(".nextcloud_oauth_shared_test_client.json")
|
||||||
|
client_info = load_client_from_file(storage_path)
|
||||||
|
|
||||||
|
if not client_info and not registration_endpoint:
|
||||||
|
raise ValueError(
|
||||||
|
"Cannot create OAuth client: registration_endpoint not available and no pre-existing credentials found at .nextcloud_oauth_shared_test_client.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not client_info:
|
||||||
|
# Register or load shared OAuth client (matches MCP server registration)
|
||||||
|
client_info = await load_or_register_client(
|
||||||
|
nextcloud_url=nextcloud_host,
|
||||||
|
registration_endpoint=registration_endpoint,
|
||||||
|
storage_path=".nextcloud_oauth_shared_test_client.json",
|
||||||
|
client_name="Pytest - Shared Test Client",
|
||||||
|
redirect_uris=[callback_url],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f"Using existing shared OAuth client: {client_info.client_id[:16]}..."
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Shared OAuth client ready: {client_info.client_id[:16]}...")
|
||||||
|
logger.info("This client will be reused for all test user authentications")
|
||||||
|
|
||||||
|
return (
|
||||||
|
client_info.client_id,
|
||||||
|
client_info.client_secret,
|
||||||
|
callback_url,
|
||||||
|
token_endpoint,
|
||||||
|
authorization_endpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
async def shared_jwt_oauth_client_credentials(anyio_backend, oauth_callback_server):
|
||||||
|
"""
|
||||||
|
Fixture to obtain shared JWT OAuth client credentials for JWT MCP server.
|
||||||
|
|
||||||
|
Creates a JWT OAuth client with full scopes (nc:read and nc:write) for use with
|
||||||
|
the JWT MCP server (port 8002) that validates JWT tokens locally.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (client_id, client_secret, callback_url, token_endpoint, authorization_endpoint)
|
||||||
|
"""
|
||||||
|
nextcloud_host = os.getenv("NEXTCLOUD_HOST")
|
||||||
|
if not nextcloud_host:
|
||||||
|
pytest.skip("Shared JWT OAuth client requires NEXTCLOUD_HOST")
|
||||||
|
|
||||||
|
# Get callback URL from the real callback server
|
||||||
|
auth_states, callback_url = oauth_callback_server
|
||||||
|
|
||||||
|
logger.info("Setting up shared JWT OAuth client credentials...")
|
||||||
|
logger.info(f"Using real callback server at: {callback_url}")
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=30.0) as http_client:
|
async with httpx.AsyncClient(timeout=30.0) as http_client:
|
||||||
# OIDC Discovery
|
# OIDC Discovery
|
||||||
discovery_url = f"{nextcloud_host}/.well-known/openid-configuration"
|
discovery_url = f"{nextcloud_host}/.well-known/openid-configuration"
|
||||||
@@ -917,16 +995,15 @@ async def shared_oauth_client_credentials(anyio_backend, oauth_callback_server):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Create JWT client with full scopes (nc:read and nc:write)
|
# Create JWT client with full scopes (nc:read and nc:write)
|
||||||
# This matches what the authorization request in playwright_oauth_token requests
|
|
||||||
client_id, client_secret = await _create_oauth_client_with_scopes(
|
client_id, client_secret = await _create_oauth_client_with_scopes(
|
||||||
callback_url=callback_url,
|
callback_url=callback_url,
|
||||||
client_name="Pytest - Shared Test Client (JWT)",
|
client_name="Pytest - Shared JWT Test Client",
|
||||||
allowed_scopes="openid profile email nc:read nc:write",
|
allowed_scopes="openid profile email nc:read nc:write",
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Shared JWT OAuth client ready: {client_id[:16]}...")
|
logger.info(f"Shared JWT OAuth client ready: {client_id[:16]}...")
|
||||||
logger.info(
|
logger.info(
|
||||||
"This JWT client with full scopes will be reused for all test user authentications"
|
"This JWT client with full scopes will be reused for JWT MCP server tests"
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1282,6 +1359,27 @@ async def playwright_oauth_token(
|
|||||||
return access_token
|
return access_token
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
async def playwright_oauth_token_jwt(
|
||||||
|
anyio_backend, browser, shared_jwt_oauth_client_credentials, oauth_callback_server
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Fixture to obtain a JWT OAuth access token for the JWT MCP server.
|
||||||
|
|
||||||
|
Uses a JWT OAuth client with full scopes (nc:read and nc:write) to ensure
|
||||||
|
the access token includes proper scope claims that the JWT MCP server can validate.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JWT access token string
|
||||||
|
"""
|
||||||
|
return await _get_oauth_token_with_scopes(
|
||||||
|
browser,
|
||||||
|
shared_jwt_oauth_client_credentials,
|
||||||
|
oauth_callback_server,
|
||||||
|
scopes="openid profile email nc:read nc:write",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _get_oauth_token_with_scopes(
|
async def _get_oauth_token_with_scopes(
|
||||||
browser,
|
browser,
|
||||||
shared_oauth_client_credentials,
|
shared_oauth_client_credentials,
|
||||||
|
|||||||
Reference in New Issue
Block a user