From 2c35e07675c600e2df862ba634e300041844a148 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Wed, 22 Oct 2025 07:17:40 +0200 Subject: [PATCH] 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 --- tests/conftest.py | 114 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 106 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index fa2b446..a3c3ce3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -187,7 +187,7 @@ async def nc_mcp_oauth_client( @pytest.fixture(scope="session") async def nc_mcp_oauth_jwt_client( anyio_backend, - playwright_oauth_token: str, + playwright_oauth_token_jwt: str, ) -> AsyncGenerator[ClientSession, Any]: """ 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( url="http://127.0.0.1:8002/mcp", - token=playwright_oauth_token, + token=playwright_oauth_token_jwt, client_name="OAuth JWT MCP (Playwright)", ): 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. - Creates a JWT OAuth client with full scopes (nc:read and nc:write) to match the MCP server's - behavior. This allows all test users to authenticate using the same client_id/secret. + This registers a single OAuth client with Nextcloud that matches the MCP server's + 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: 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") if not 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(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: # OIDC Discovery 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) - # This matches what the authorization request in playwright_oauth_token requests client_id, client_secret = await _create_oauth_client_with_scopes( 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", ) logger.info(f"Shared JWT OAuth client ready: {client_id[:16]}...") 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 ( @@ -1282,6 +1359,27 @@ async def playwright_oauth_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( browser, shared_oauth_client_credentials,