chore: Move httpd server to separate fixture

This commit is contained in:
Chris Coutinho
2025-10-14 01:23:32 +02:00
parent b26ff4f9bc
commit b3b7c90bd0
2 changed files with 51 additions and 23 deletions
+3
View File
@@ -28,6 +28,9 @@ markers = [
"integration: marks tests as slow (deselect with '-m \"not slow\"')", "integration: marks tests as slow (deselect with '-m \"not slow\"')",
"oauth: marks tests as oauth (deselect with '-m \"not oauth\"')" "oauth: marks tests as oauth (deselect with '-m \"not oauth\"')"
] ]
testpaths = [
"tests",
]
[tool.commitizen] [tool.commitizen]
name = "cz_conventional_commits" name = "cz_conventional_commits"
+48 -23
View File
@@ -632,18 +632,19 @@ async def nc_oauth_client(
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
async def interactive_oauth_token() -> str: def oauth_callback_server():
""" """
Fixture to obtain an OAuth access token for integration tests. Fixture to create an HTTP server for OAuth callback handling.
This uses the interactive OAuth flow to get a token. Yields a tuple of (auth_state, server_url) where:
- auth_state: A dict with {"code": None} that will be populated with the auth code
- server_url: The callback URL for the server (e.g., "http://localhost:8081")
The server automatically shuts down when the fixture is torn down.
""" """
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
import threading import threading
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
import time
# Use a mutable container to share state across threads # Use a mutable container to share state across threads
auth_state = {"code": None} auth_state = {"code": None}
@@ -688,13 +689,46 @@ async def interactive_oauth_token() -> str:
self.send_response(404) self.send_response(404)
self.end_headers() self.end_headers()
httpd = HTTPServer(("localhost", 8081), OAuthCallbackHandler) try:
server_thread = threading.Thread(target=httpd.serve_forever) # Start the HTTP server
server_thread.daemon = True httpd = HTTPServer(("localhost", 8081), OAuthCallbackHandler)
server_thread.start() server_thread = threading.Thread(target=httpd.serve_forever)
server_thread.daemon = True
server_thread.start()
logger.info("OAuth callback server started on http://localhost:8081")
# Yield the auth state and server URL
yield auth_state, "http://localhost:8081"
finally:
# Clean up the server
if httpd:
logger.info("Shutting down OAuth callback server...")
shutdown_thread = threading.Thread(target=httpd.shutdown)
shutdown_thread.start()
shutdown_thread.join(timeout=2) # Wait up to 2 seconds for shutdown
httpd.server_close()
logger.info("OAuth callback server shut down successfully")
if server_thread:
server_thread.join(timeout=1)
@pytest.fixture(scope="session")
async def interactive_oauth_token(oauth_callback_server) -> str:
"""
Fixture to obtain an OAuth access token for integration tests.
This uses the interactive OAuth flow to get a token.
Depends on oauth_callback_server fixture for HTTP callback handling.
"""
import webbrowser
import time
from nextcloud_mcp_server.auth.client_registration import load_or_register_client from nextcloud_mcp_server.auth.client_registration import load_or_register_client
# Unpack the server fixture
auth_state, callback_url = oauth_callback_server
nextcloud_host = os.getenv("NEXTCLOUD_HOST") nextcloud_host = os.getenv("NEXTCLOUD_HOST")
async with httpx.AsyncClient() as http_client: async with httpx.AsyncClient() as http_client:
discovery_url = f"{nextcloud_host}/.well-known/openid-configuration" discovery_url = f"{nextcloud_host}/.well-known/openid-configuration"
@@ -707,7 +741,7 @@ async def interactive_oauth_token() -> str:
nextcloud_url=nextcloud_host, nextcloud_url=nextcloud_host,
registration_endpoint=registration_endpoint, registration_endpoint=registration_endpoint,
storage_path=".nextcloud_oauth_test_client.json", storage_path=".nextcloud_oauth_test_client.json",
redirect_uris=["http://localhost:8081"], redirect_uris=[callback_url],
force_register=True, force_register=True,
) )
@@ -719,11 +753,9 @@ async def interactive_oauth_token() -> str:
) )
# Construct authorization URL # Construct authorization URL
auth_url = f"{authorization_endpoint}?response_type=code&client_id={client_info.client_id}&redirect_uri=http://localhost:8081&scope=openid%20profile%20email" auth_url = f"{authorization_endpoint}?response_type=code&client_id={client_info.client_id}&redirect_uri={callback_url}&scope=openid%20profile%20email"
# Open login page first, then auth URL # Open authorization URL in browser
# webbrowser.open(login_url)
# time.sleep(2) # Give browser time to load login page
webbrowser.open(auth_url) webbrowser.open(auth_url)
# Wait for auth code with timeout # Wait for auth code with timeout
@@ -743,7 +775,7 @@ async def interactive_oauth_token() -> str:
data={ data={
"grant_type": "authorization_code", "grant_type": "authorization_code",
"code": auth_code, "code": auth_code,
"redirect_uri": "http://localhost:8081", "redirect_uri": callback_url,
"client_id": client_info.client_id, "client_id": client_info.client_id,
"client_secret": client_info.client_secret, "client_secret": client_info.client_secret,
}, },
@@ -754,11 +786,4 @@ async def interactive_oauth_token() -> str:
logger.debug(f"Token data: {token_data}") logger.debug(f"Token data: {token_data}")
access_token = token_data.get("access_token") access_token = token_data.get("access_token")
# Shut down the server
# Call shutdown directly instead of via HTTP to avoid race conditions
if httpd:
httpd.shutdown()
httpd.server_close()
if server_thread:
server_thread.join(timeout=1)
return access_token return access_token