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
|
- **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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user