test: Enable tests via playwright, disable interactive in CI

This commit is contained in:
Chris Coutinho
2025-10-14 01:23:38 +02:00
parent 37b0577bfd
commit 6ce411094c
4 changed files with 161 additions and 30 deletions
+10 -9
View File
@@ -118,35 +118,36 @@ Each Nextcloud app has a corresponding server module that:
- **Avoid creating standalone test scripts** - use pytest with proper fixtures instead - **Avoid creating standalone test scripts** - use pytest with proper fixtures instead
#### OAuth/OIDC Testing #### OAuth/OIDC Testing
OAuth integration tests support both **interactive** and **automated** (Playwright) authentication flows: OAuth integration tests support both **automated** (Playwright) and **interactive** authentication flows:
**Automated Testing (Recommended for CI/CD):** **Automated Testing (Default - Recommended for CI/CD):**
- **Default fixtures**: `nc_oauth_client`, `nc_mcp_oauth_client` now use Playwright automation by default
- Uses Playwright headless browser automation to complete OAuth flow programmatically - Uses Playwright headless browser automation to complete OAuth flow programmatically
- Fixtures: `playwright_oauth_token`, `nc_oauth_client_playwright`, `nc_mcp_oauth_client_playwright` - All Playwright fixtures: `playwright_oauth_token`, `nc_oauth_client`, `nc_mcp_oauth_client`, `nc_oauth_client_playwright`, `nc_mcp_oauth_client_playwright`
- Requires: `NEXTCLOUD_HOST`, `NEXTCLOUD_USERNAME`, `NEXTCLOUD_PASSWORD` environment variables - Requires: `NEXTCLOUD_HOST`, `NEXTCLOUD_USERNAME`, `NEXTCLOUD_PASSWORD` environment variables
- Uses `pytest-playwright-asyncio` for async Playwright fixtures - Uses `pytest-playwright-asyncio` for async Playwright fixtures
- Playwright configuration: Use pytest CLI args like `--browser firefox --headed` to customize - Playwright configuration: Use pytest CLI args like `--browser firefox --headed` to customize
- Install browsers: `uv run playwright install firefox` (or `chromium`, `webkit`) - Install browsers: `uv run playwright install firefox` (or `chromium`, `webkit`)
- Example: - Example:
```bash ```bash
# Run OAuth tests with automated Playwright flow using Firefox # Run all OAuth tests with automated Playwright flow using Firefox
uv run pytest tests/integration/test_oauth_playwright.py --browser firefox -v uv run pytest tests/integration/test_oauth*.py --browser firefox -v
# Run with visible browser for debugging # Run specific Playwright tests with visible browser for debugging
uv run pytest tests/integration/test_oauth_playwright.py --browser firefox --headed -v uv run pytest tests/integration/test_oauth_playwright.py --browser firefox --headed -v
# Run with Chromium (default) # Run with Chromium (default)
uv run pytest tests/integration/test_oauth_playwright.py -v uv run pytest tests/integration/test_oauth.py -v
``` ```
**Interactive Testing (Manual browser login):** **Interactive Testing (Manual browser login):**
- Opens system browser and waits for manual login/authorization - Opens system browser and waits for manual login/authorization
- Fixtures: `interactive_oauth_token`, `nc_oauth_client`, `nc_mcp_oauth_client` - Fixtures: `interactive_oauth_token`, `nc_oauth_client_interactive`, `nc_mcp_oauth_client_interactive`
- Requires: User to complete browser-based login when prompted - Requires: User to complete browser-based login when prompted
- Useful for: Debugging OAuth flows, testing with 2FA, local development - Useful for: Debugging OAuth flows, testing with 2FA, local development
- Example: - Example:
```bash ```bash
# Run OAuth tests with interactive flow (will open browser) # Run OAuth tests with interactive flow (will open browser and wait for manual login)
uv run pytest tests/integration/test_oauth_interactive.py -v uv run pytest tests/integration/test_oauth_interactive.py -v
``` ```
+142 -14
View File
@@ -136,14 +136,23 @@ async def nc_mcp_client() -> AsyncGenerator[ClientSession, Any]:
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
async def nc_mcp_oauth_client( async def nc_mcp_oauth_client_interactive(
interactive_oauth_token: str, interactive_oauth_token: str,
) -> AsyncGenerator[ClientSession, Any]: ) -> AsyncGenerator[ClientSession, Any]:
""" """
Fixture to create an MCP client session for OAuth integration tests using streamable-http. Fixture to create an MCP client session for OAuth integration tests using interactive authentication.
Connects to the OAuth-enabled MCP server on port 8001 with OAuth authentication. Connects to the OAuth-enabled MCP server on port 8001 with OAuth authentication.
Requires manual browser login.
For automated testing, use nc_mcp_oauth_client fixture instead.
Automatically skips when running in GitHub Actions CI.
""" """
logger.info("Creating Streamable HTTP client for OAuth MCP server") # Skip interactive tests in CI environments
if os.getenv("GITHUB_ACTIONS"):
pytest.skip("Skipping interactive OAuth tests in GitHub Actions CI")
logger.info("Creating Streamable HTTP client for OAuth MCP server (Interactive)")
# Pass OAuth token as Bearer token in headers # Pass OAuth token as Bearer token in headers
headers = {"Authorization": f"Bearer {interactive_oauth_token}"} headers = {"Authorization": f"Bearer {interactive_oauth_token}"}
@@ -157,7 +166,7 @@ async def nc_mcp_oauth_client(
session_context = ClientSession(read_stream, write_stream) session_context = ClientSession(read_stream, write_stream)
session = await session_context.__aenter__() session = await session_context.__aenter__()
await session.initialize() await session.initialize()
logger.info("OAuth MCP client session initialized successfully") logger.info("OAuth MCP client session (Interactive) initialized successfully")
yield session yield session
@@ -170,9 +179,9 @@ async def nc_mcp_oauth_client(
if "cancel scope" in str(e): if "cancel scope" in str(e):
logger.debug(f"Ignoring cancel scope teardown issue: {e}") logger.debug(f"Ignoring cancel scope teardown issue: {e}")
else: else:
logger.warning(f"Error closing OAuth session: {e}") logger.warning(f"Error closing OAuth session (Interactive): {e}")
except Exception as e: except Exception as e:
logger.warning(f"Error closing OAuth session: {e}") logger.warning(f"Error closing OAuth session (Interactive): {e}")
try: try:
await streamable_context.__aexit__(None, None, None) await streamable_context.__aexit__(None, None, None)
@@ -180,9 +189,70 @@ async def nc_mcp_oauth_client(
if "cancel scope" in str(e): if "cancel scope" in str(e):
logger.debug(f"Ignoring cancel scope teardown issue: {e}") logger.debug(f"Ignoring cancel scope teardown issue: {e}")
else: else:
logger.warning(f"Error closing OAuth streamable HTTP client: {e}") logger.warning(
f"Error closing OAuth streamable HTTP client (Interactive): {e}"
)
except Exception as e: except Exception as e:
logger.warning(f"Error closing OAuth streamable HTTP client: {e}") logger.warning(
f"Error closing OAuth streamable HTTP client (Interactive): {e}"
)
@pytest.fixture(scope="session")
async def nc_mcp_oauth_client(
playwright_oauth_token: str,
) -> AsyncGenerator[ClientSession, Any]:
"""
Fixture to create an MCP client session for OAuth integration tests using Playwright automation.
Connects to the OAuth-enabled MCP server on port 8001 with OAuth authentication.
This is the default OAuth MCP fixture using headless browser automation suitable for CI/CD.
For interactive testing with manual browser login, use nc_mcp_oauth_client_interactive instead.
"""
logger.info("Creating Streamable HTTP client for OAuth MCP server (Playwright)")
# Pass OAuth token as Bearer token in headers
headers = {"Authorization": f"Bearer {playwright_oauth_token}"}
streamable_context = streamablehttp_client(
"http://127.0.0.1:8001/mcp", headers=headers
)
session_context = None
try:
read_stream, write_stream, _ = await streamable_context.__aenter__()
session_context = ClientSession(read_stream, write_stream)
session = await session_context.__aenter__()
await session.initialize()
logger.info("OAuth MCP client session (Playwright) initialized successfully")
yield session
finally:
# Clean up in reverse order, ignoring task scope issues
if session_context is not None:
try:
await session_context.__aexit__(None, None, None)
except RuntimeError as e:
if "cancel scope" in str(e):
logger.debug(f"Ignoring cancel scope teardown issue: {e}")
else:
logger.warning(f"Error closing Playwright OAuth session: {e}")
except Exception as e:
logger.warning(f"Error closing Playwright OAuth session: {e}")
try:
await streamable_context.__aexit__(None, None, None)
except RuntimeError as e:
if "cancel scope" in str(e):
logger.debug(f"Ignoring cancel scope teardown issue: {e}")
else:
logger.warning(
f"Error closing Playwright OAuth streamable HTTP client: {e}"
)
except Exception as e:
logger.warning(
f"Error closing Playwright OAuth streamable HTTP client: {e}"
)
@pytest.fixture @pytest.fixture
@@ -606,20 +676,28 @@ async def oauth_token() -> str:
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
async def nc_oauth_client( async def nc_oauth_client_interactive(
interactive_oauth_token: str, interactive_oauth_token: str,
) -> AsyncGenerator[NextcloudClient, Any]: ) -> AsyncGenerator[NextcloudClient, Any]:
""" """
Fixture to create a NextcloudClient instance using OAuth authentication. Fixture to create a NextcloudClient instance using interactive OAuth authentication.
Uses the oauth_token fixture to get an access token. Uses the interactive_oauth_token fixture which requires manual browser login.
For automated testing, use nc_oauth_client fixture instead.
Automatically skips when running in GitHub Actions CI.
""" """
# Skip interactive tests in CI environments
if os.getenv("GITHUB_ACTIONS"):
pytest.skip("Skipping interactive OAuth tests in GitHub Actions CI")
nextcloud_host = os.getenv("NEXTCLOUD_HOST") nextcloud_host = os.getenv("NEXTCLOUD_HOST")
username = os.getenv("NEXTCLOUD_USERNAME") username = os.getenv("NEXTCLOUD_USERNAME")
if not all([nextcloud_host, username]): if not all([nextcloud_host, username]):
pytest.skip("OAuth client fixture requires NEXTCLOUD_HOST and USERNAME") pytest.skip("OAuth client fixture requires NEXTCLOUD_HOST and USERNAME")
logger.info(f"Creating OAuth NextcloudClient for user: {username}") logger.info(f"Creating OAuth NextcloudClient (Interactive) for user: {username}")
client = NextcloudClient.from_token( client = NextcloudClient.from_token(
base_url=nextcloud_host, base_url=nextcloud_host,
token=interactive_oauth_token, token=interactive_oauth_token,
@@ -629,15 +707,54 @@ async def nc_oauth_client(
# Verify the OAuth client works # Verify the OAuth client works
try: try:
await client.capabilities() await client.capabilities()
logger.info("OAuth NextcloudClient initialized and capabilities checked.") logger.info(
"OAuth NextcloudClient (Interactive) initialized and capabilities checked."
)
yield client yield client
except Exception as e: except Exception as e:
logger.error(f"Failed to initialize OAuth NextcloudClient: {e}") logger.error(f"Failed to initialize OAuth NextcloudClient (Interactive): {e}")
pytest.fail(f"Failed to connect to Nextcloud with OAuth token: {e}") pytest.fail(f"Failed to connect to Nextcloud with OAuth token: {e}")
finally: finally:
await client.close() await client.close()
@pytest.fixture(scope="session")
async def nc_oauth_client(
playwright_oauth_token: str,
) -> AsyncGenerator[NextcloudClient, Any]:
"""
Fixture to create a NextcloudClient instance using automated Playwright OAuth authentication.
This is the default OAuth fixture using headless browser automation suitable for CI/CD.
For interactive testing with manual browser login, use nc_oauth_client_interactive instead.
"""
nextcloud_host = os.getenv("NEXTCLOUD_HOST")
username = os.getenv("NEXTCLOUD_USERNAME")
if not all([nextcloud_host, username]):
pytest.skip("OAuth client fixture requires NEXTCLOUD_HOST and USERNAME")
logger.info(f"Creating OAuth NextcloudClient (Playwright) for user: {username}")
client = NextcloudClient.from_token(
base_url=nextcloud_host,
token=playwright_oauth_token,
username=username,
)
# Verify the OAuth client works
try:
await client.capabilities()
logger.info(
"OAuth NextcloudClient (Playwright) initialized and capabilities checked."
)
yield client
except Exception as e:
logger.error(f"Failed to initialize OAuth NextcloudClient (Playwright): {e}")
pytest.fail(f"Failed to connect to Nextcloud with Playwright OAuth token: {e}")
finally:
await client.close()
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def oauth_callback_server(): def oauth_callback_server():
""" """
@@ -648,7 +765,12 @@ def oauth_callback_server():
- server_url: The callback URL for the server (e.g., "http://localhost:8081") - server_url: The callback URL for the server (e.g., "http://localhost:8081")
The server automatically shuts down when the fixture is torn down. The server automatically shuts down when the fixture is torn down.
Automatically skips when running in GitHub Actions CI.
""" """
# Skip interactive tests in CI environments
if os.getenv("GITHUB_ACTIONS"):
pytest.skip("Skipping interactive OAuth tests in GitHub Actions CI")
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
@@ -727,7 +849,13 @@ async def interactive_oauth_token(oauth_callback_server) -> str:
This uses the interactive OAuth flow to get a token. This uses the interactive OAuth flow to get a token.
Depends on oauth_callback_server fixture for HTTP callback handling. Depends on oauth_callback_server fixture for HTTP callback handling.
Automatically skips when running in GitHub Actions CI.
""" """
# Skip interactive tests in CI environments
if os.getenv("GITHUB_ACTIONS"):
pytest.skip("Skipping interactive OAuth tests in GitHub Actions CI")
import webbrowser import webbrowser
import time import time
+1 -1
View File
@@ -66,7 +66,7 @@ async def test_oauth_client_create_note(nc_oauth_client: NextcloudClient):
async def test_token_in_request_headers( async def test_token_in_request_headers(
nc_oauth_client: NextcloudClient, interactive_oauth_token: str nc_oauth_client: NextcloudClient, playwright_oauth_token: str
): ):
"""Verify that bearer token is being used in requests.""" """Verify that bearer token is being used in requests."""
# The client should be using BearerAuth # The client should be using BearerAuth
+8 -6
View File
@@ -10,24 +10,26 @@ pytestmark = [pytest.mark.integration, pytest.mark.oauth]
class TestOAuthInteractive: class TestOAuthInteractive:
"""Test interactive OAuth authentication.""" """Test interactive OAuth authentication with manual browser login."""
async def test_oauth_client_with_interactive_flow(self, nc_oauth_client): async def test_oauth_client_with_interactive_flow(
self, nc_oauth_client_interactive
):
"""Test that OAuth client created via interactive flow can access Nextcloud APIs.""" """Test that OAuth client created via interactive flow can access Nextcloud APIs."""
# Test 1: Check capabilities # Test 1: Check capabilities
capabilities = await nc_oauth_client.capabilities() capabilities = await nc_oauth_client_interactive.capabilities()
assert capabilities is not None assert capabilities is not None
logger.info("OAuth client (interactive) successfully fetched capabilities") logger.info("OAuth client (interactive) successfully fetched capabilities")
# Test 2: List notes # Test 2: List notes
notes = await nc_oauth_client.notes.get_all_notes() notes = await nc_oauth_client_interactive.notes.get_all_notes()
assert isinstance(notes, list) assert isinstance(notes, list)
logger.info( logger.info(
f"OAuth client (interactive) successfully listed {len(notes)} notes" f"OAuth client (interactive) successfully listed {len(notes)} notes"
) )
# Test 3: Create and delete a note # Test 3: Create and delete a note
test_note = await nc_oauth_client.notes.create_note( test_note = await nc_oauth_client_interactive.notes.create_note(
title="OAuth Interactive Test Note", title="OAuth Interactive Test Note",
content="This note was created during OAuth interactive testing", content="This note was created during OAuth interactive testing",
) )
@@ -37,5 +39,5 @@ class TestOAuthInteractive:
logger.info(f"OAuth client (interactive) successfully created note {note_id}") logger.info(f"OAuth client (interactive) successfully created note {note_id}")
# Clean up # Clean up
await nc_oauth_client.notes.delete_note(note_id=note_id) await nc_oauth_client_interactive.notes.delete_note(note_id=note_id)
logger.info(f"OAuth client (interactive) successfully deleted note {note_id}") logger.info(f"OAuth client (interactive) successfully deleted note {note_id}")