test: Enable tests via playwright, disable interactive in CI

This commit is contained in:
Chris Coutinho
2025-10-13 19:22:59 +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
#### 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
- 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
- Uses `pytest-playwright-asyncio` for async Playwright fixtures
- Playwright configuration: Use pytest CLI args like `--browser firefox --headed` to customize
- Install browsers: `uv run playwright install firefox` (or `chromium`, `webkit`)
- Example:
```bash
# Run OAuth tests with automated Playwright flow using Firefox
uv run pytest tests/integration/test_oauth_playwright.py --browser firefox -v
# Run all OAuth tests with automated Playwright flow using Firefox
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
# 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):**
- 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
- Useful for: Debugging OAuth flows, testing with 2FA, local development
- Example:
```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
```
+142 -14
View File
@@ -136,14 +136,23 @@ async def nc_mcp_client() -> AsyncGenerator[ClientSession, Any]:
@pytest.fixture(scope="session")
async def nc_mcp_oauth_client(
async def nc_mcp_oauth_client_interactive(
interactive_oauth_token: str,
) -> 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.
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
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 = await session_context.__aenter__()
await session.initialize()
logger.info("OAuth MCP client session initialized successfully")
logger.info("OAuth MCP client session (Interactive) initialized successfully")
yield session
@@ -170,9 +179,9 @@ async def nc_mcp_oauth_client(
if "cancel scope" in str(e):
logger.debug(f"Ignoring cancel scope teardown issue: {e}")
else:
logger.warning(f"Error closing OAuth session: {e}")
logger.warning(f"Error closing OAuth session (Interactive): {e}")
except Exception as e:
logger.warning(f"Error closing OAuth session: {e}")
logger.warning(f"Error closing OAuth session (Interactive): {e}")
try:
await streamable_context.__aexit__(None, None, None)
@@ -180,9 +189,70 @@ async def nc_mcp_oauth_client(
if "cancel scope" in str(e):
logger.debug(f"Ignoring cancel scope teardown issue: {e}")
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:
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
@@ -606,20 +676,28 @@ async def oauth_token() -> str:
@pytest.fixture(scope="session")
async def nc_oauth_client(
async def nc_oauth_client_interactive(
interactive_oauth_token: str,
) -> AsyncGenerator[NextcloudClient, Any]:
"""
Fixture to create a NextcloudClient instance using OAuth authentication.
Uses the oauth_token fixture to get an access token.
Fixture to create a NextcloudClient instance using interactive OAuth authentication.
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")
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 for user: {username}")
logger.info(f"Creating OAuth NextcloudClient (Interactive) for user: {username}")
client = NextcloudClient.from_token(
base_url=nextcloud_host,
token=interactive_oauth_token,
@@ -629,15 +707,54 @@ async def nc_oauth_client(
# Verify the OAuth client works
try:
await client.capabilities()
logger.info("OAuth NextcloudClient initialized and capabilities checked.")
logger.info(
"OAuth NextcloudClient (Interactive) initialized and capabilities checked."
)
yield client
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}")
finally:
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")
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")
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
import threading
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.
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 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(
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."""
# The client should be using BearerAuth
+8 -6
View File
@@ -10,24 +10,26 @@ pytestmark = [pytest.mark.integration, pytest.mark.oauth]
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 1: Check capabilities
capabilities = await nc_oauth_client.capabilities()
capabilities = await nc_oauth_client_interactive.capabilities()
assert capabilities is not None
logger.info("OAuth client (interactive) successfully fetched capabilities")
# 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)
logger.info(
f"OAuth client (interactive) successfully listed {len(notes)} notes"
)
# 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",
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}")
# 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}")