test: Enable tests via playwright, disable interactive in CI
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user