Compare commits

...

4 Commits

Author SHA1 Message Date
github-actions[bot] 6734de8389 bump: version 0.14.2 → 0.14.3 2025-10-17 00:04:25 +00:00
Chris Coutinho 3cb31d07f1 Merge pull request #214 from cbcoutinho/renovate/mcp-1.x
fix(deps): update dependency mcp to >=1.18,<1.19
2025-10-17 02:04:00 +02:00
renovate-bot-cbcoutinho[bot] 16b9123af3 fix(deps): update dependency mcp to >=1.18,<1.19 2025-10-16 19:20:47 +00:00
Chris Coutinho 51d1f075f5 test: Remove duplicated/interactive testing fixtures
All integration tests now run without interactive browser usage, simplifying CI and testing infrastructure
2025-10-16 19:46:29 +02:00
8 changed files with 41 additions and 292 deletions
+6
View File
@@ -1,3 +1,9 @@
## v0.14.3 (2025-10-17)
### Fix
- **deps**: update dependency mcp to >=1.18,<1.19
## v0.14.2 (2025-10-16) ## v0.14.2 (2025-10-16)
### Fix ### Fix
+21 -34
View File
@@ -132,47 +132,35 @@ 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 **automated** (Playwright) and **interactive** authentication flows: OAuth integration tests use **automated Playwright browser automation** to complete the OAuth flow programmatically.
**Automated Testing (Default - Recommended for CI/CD):** **OAuth Testing Setup:**
- **Default fixtures**: `nc_oauth_client`, `nc_mcp_oauth_client` use Playwright automation - **Main fixtures**: `nc_oauth_client`, `nc_mcp_oauth_client` - Use Playwright automation
- Uses Playwright headless browser automation to complete OAuth flow programmatically
- **Shared OAuth Client**: All test users authenticate using a single OAuth client - **Shared OAuth Client**: All test users authenticate using a single OAuth client
- Stored in `.nextcloud_oauth_shared_test_client.json` - Stored in `.nextcloud_oauth_shared_test_client.json`
- Matches production MCP server behavior - Matches production MCP server behavior
- Each user gets their own unique access token - Each user gets their own unique access token
- Implementation: `shared_oauth_client_credentials` fixture in `tests/conftest.py:812` - Implementation: `shared_oauth_client_credentials` fixture in `tests/conftest.py`
- All Playwright fixtures: `playwright_oauth_token`, `nc_oauth_client`, `nc_mcp_oauth_client`, `nc_oauth_client_playwright`, `nc_mcp_oauth_client_playwright` - **Available fixtures**: `playwright_oauth_token`, `nc_oauth_client`, `nc_mcp_oauth_client`
- Multi-user fixtures: `alice_oauth_token`, `bob_oauth_token`, `charlie_oauth_token`, `diana_oauth_token` - **Multi-user fixtures**: `alice_oauth_token`, `bob_oauth_token`, `charlie_oauth_token`, `diana_oauth_token`
- Requires: `NEXTCLOUD_HOST`, `NEXTCLOUD_USERNAME`, `NEXTCLOUD_PASSWORD` environment variables - **Requirements**: `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:
```bash
# Run all OAuth tests with automated Playwright flow using Firefox
uv run pytest tests/server/test_oauth*.py --browser firefox -v
# Run specific Playwright tests with visible browser for debugging **Example Commands:**
uv run pytest tests/server/test_mcp_oauth.py --browser firefox --headed -v ```bash
# Run all OAuth tests with Playwright automation using Firefox
uv run pytest tests/server/test_oauth*.py --browser firefox -v
# Run with Chromium (default) # Run specific tests with visible browser for debugging
uv run pytest tests/server/test_oauth*.py -v uv run pytest tests/server/test_mcp_oauth.py --browser firefox --headed -v
```
**Interactive Testing (Manual browser login):** # Run with Chromium (default)
- Opens system browser and waits for manual login/authorization uv run pytest tests/server/test_oauth*.py -v
- 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
- **Automatically skipped in GitHub Actions CI** - Interactive fixtures check for `GITHUB_ACTIONS` environment variable
- Example:
```bash
# Run OAuth tests with interactive flow (will open browser and wait for manual login)
uv run pytest tests/client/test_oauth_interactive.py -v
```
**Test Environment Setup:** **Test Environment:**
- **Two MCP server containers are available:** - **Two MCP server containers are available:**
- `mcp` (port 8000): Uses basic auth with admin credentials - for most testing - `mcp` (port 8000): Uses basic auth with admin credentials - for most testing
- `mcp-oauth` (port 8001): Uses OAuth authentication - for OAuth-specific testing - `mcp-oauth` (port 8001): Uses OAuth authentication - for OAuth-specific testing
@@ -180,9 +168,8 @@ OAuth integration tests support both **automated** (Playwright) and **interactiv
- **Important**: When working on OAuth functionality, always rebuild `mcp-oauth` container, not `mcp` - **Important**: When working on OAuth functionality, always rebuild `mcp-oauth` container, not `mcp`
- OAuth client credentials cached in `.nextcloud_oauth_shared_test_client.json` - OAuth client credentials cached in `.nextcloud_oauth_shared_test_client.json`
**CI/CD Considerations:** **CI/CD Notes:**
- Interactive OAuth tests are automatically skipped when `GITHUB_ACTIONS` environment variable is set - Playwright tests run in CI/CD environments
- Automated Playwright tests will run in CI/CD environments
- Use Firefox browser in CI: `--browser firefox` (Chromium may have issues with localhost redirects) - Use Firefox browser in CI: `--browser firefox` (Chromium may have issues with localhost redirects)
### Configuration Files ### Configuration Files
+2 -2
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "nextcloud-mcp-server" name = "nextcloud-mcp-server"
version = "0.14.2" version = "0.14.3"
description = "" description = ""
authors = [ authors = [
{name = "Chris Coutinho",email = "chris@coutinho.io"} {name = "Chris Coutinho",email = "chris@coutinho.io"}
@@ -8,7 +8,7 @@ authors = [
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
"mcp[cli] (>=1.17,<1.18)", "mcp[cli] (>=1.18,<1.19)",
"httpx (>=0.28.1,<0.29.0)", "httpx (>=0.28.1,<0.29.0)",
"pillow (>=12.0.0,<12.1.0)", "pillow (>=12.0.0,<12.1.0)",
"icalendar (>=6.0.0,<7.0.0)", "icalendar (>=6.0.0,<7.0.0)",
-41
View File
@@ -1,41 +0,0 @@
"""Interactive integration tests for OAuth authentication."""
import logging
import os
import pytest
logger = logging.getLogger(__name__)
pytestmark = [pytest.mark.integration, pytest.mark.oauth]
@pytest.mark.skipif(
"GITHUB_ACTIONS" in os.environ,
reason="Unable to access interactive browser in GitHub Actions",
)
async def test_oauth_client_with_interactive_flow(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_interactive.capabilities()
assert capabilities is not None
logger.info("OAuth client (interactive) successfully fetched capabilities")
# Test 2: List 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_interactive.notes.create_note(
title="OAuth Interactive Test Note",
content="This note was created during OAuth interactive testing",
)
assert test_note is not None
assert test_note.get("id") is not None
note_id = test_note["id"]
logger.info(f"OAuth client (interactive) successfully created note {note_id}")
# Clean up
await nc_oauth_client_interactive.notes.delete_note(note_id=note_id)
logger.info(f"OAuth client (interactive) successfully deleted note {note_id}")
+3 -3
View File
@@ -19,14 +19,14 @@ async def test_playwright_oauth_token_acquisition(playwright_oauth_token: str):
) )
async def test_oauth_client_with_playwright_flow(nc_oauth_client_playwright): async def test_oauth_client_with_playwright_flow(nc_oauth_client):
"""Test that OAuth client created via Playwright flow can access Nextcloud APIs.""" """Test that OAuth client created via Playwright flow can access Nextcloud APIs."""
# Test 1: Check capabilities # Test 1: Check capabilities
capabilities = await nc_oauth_client_playwright.capabilities() capabilities = await nc_oauth_client.capabilities()
assert capabilities is not None assert capabilities is not None
logger.info("OAuth client (Playwright) successfully fetched capabilities") logger.info("OAuth client (Playwright) successfully fetched capabilities")
# Test 2: List notes # Test 2: List notes
notes = await nc_oauth_client_playwright.notes.get_all_notes() notes = await nc_oauth_client.notes.get_all_notes()
assert isinstance(notes, list) assert isinstance(notes, list)
logger.info(f"OAuth client (Playwright) successfully listed {len(notes)} notes") logger.info(f"OAuth client (Playwright) successfully listed {len(notes)} notes")
+2 -205
View File
@@ -167,27 +167,6 @@ async def nc_mcp_client() -> AsyncGenerator[ClientSession, Any]:
yield session yield session
@pytest.fixture(scope="session")
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 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.
"""
async for session in create_mcp_client_session(
url="http://127.0.0.1:8001/mcp",
token=interactive_oauth_token,
client_name="OAuth MCP (Interactive)",
):
yield session
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
async def nc_mcp_oauth_client( async def nc_mcp_oauth_client(
playwright_oauth_token: str, playwright_oauth_token: str,
@@ -196,8 +175,7 @@ async def nc_mcp_oauth_client(
Fixture to create an MCP client session for OAuth integration tests using Playwright automation. 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. 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. Uses headless browser automation suitable for CI/CD.
For interactive testing with manual browser login, use nc_mcp_oauth_client_interactive instead.
""" """
async for session in create_mcp_client_session( async for session in create_mcp_client_session(
url="http://127.0.0.1:8001/mcp", url="http://127.0.0.1:8001/mcp",
@@ -524,55 +502,13 @@ async def temporary_board_with_card(
logger.error(f"Unexpected error deleting temporary card {card.id}: {e}") logger.error(f"Unexpected error deleting temporary card {card.id}: {e}")
@pytest.fixture(scope="session")
async def nc_oauth_client_interactive(
interactive_oauth_token: str,
) -> AsyncGenerator[NextcloudClient, Any]:
"""
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.
"""
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 (Interactive) for user: {username}")
client = NextcloudClient.from_token(
base_url=nextcloud_host,
token=interactive_oauth_token,
username=username,
)
# Verify the OAuth client works
try:
await client.capabilities()
logger.info(
"OAuth NextcloudClient (Interactive) initialized and capabilities checked."
)
yield client
except Exception as 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") @pytest.fixture(scope="session")
async def nc_oauth_client( async def nc_oauth_client(
playwright_oauth_token: str, playwright_oauth_token: str,
) -> AsyncGenerator[NextcloudClient, Any]: ) -> AsyncGenerator[NextcloudClient, Any]:
""" """
Fixture to create a NextcloudClient instance using automated Playwright OAuth authentication. 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. Uses 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") nextcloud_host = os.getenv("NEXTCLOUD_HOST")
username = os.getenv("NEXTCLOUD_USERNAME") username = os.getenv("NEXTCLOUD_USERNAME")
@@ -689,84 +625,6 @@ def oauth_callback_server():
server_thread.join(timeout=1) 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.
Automatically skips when running in GitHub Actions CI.
"""
import time
import webbrowser
from nextcloud_mcp_server.auth.client_registration import load_or_register_client
# Unpack the server fixture (now returns dict of auth_states)
auth_states, callback_url = oauth_callback_server
nextcloud_host = os.getenv("NEXTCLOUD_HOST")
async with httpx.AsyncClient() as http_client:
discovery_url = f"{nextcloud_host}/.well-known/openid-configuration"
discovery_response = await http_client.get(discovery_url)
oidc_config = discovery_response.json()
token_endpoint = oidc_config.get("token_endpoint")
registration_endpoint = oidc_config.get("registration_endpoint")
authorization_endpoint = oidc_config.get("authorization_endpoint")
client_info = await load_or_register_client(
nextcloud_url=nextcloud_host,
registration_endpoint=registration_endpoint,
storage_path=".nextcloud_oauth_shared_test_client.json",
redirect_uris=[callback_url],
)
# First, open Nextcloud login page to establish session
login_url = f"{nextcloud_host}/login"
logger.info(f"Please log in to Nextcloud at: {login_url}")
logger.info(
"After logging in, the OAuth authorization will proceed automatically"
)
# Construct authorization URL (no state parameter for interactive flow)
auth_url = f"{authorization_endpoint}?response_type=code&client_id={client_info.client_id}&redirect_uri={callback_url}&scope=openid%20profile%20email"
# Open authorization URL in browser
webbrowser.open(auth_url)
# Wait for auth code with timeout (uses "_default" key for flows without state)
timeout = 120 # 2 minutes
start_time = time.time()
while "_default" not in auth_states:
if time.time() - start_time > timeout:
raise TimeoutError("OAuth authorization timed out after 2 minutes")
logger.info("Waiting for OAuth authorization...")
time.sleep(1)
auth_code = auth_states["_default"]
logger.info("Received authorization code, exchanging for token...")
token_response = await http_client.post(
token_endpoint,
data={
"grant_type": "authorization_code",
"code": auth_code,
"redirect_uri": callback_url,
"client_id": client_info.client_id,
"client_secret": client_info.client_secret,
},
)
logger.debug(f"Token response: {token_response.text}")
token_data = token_response.json()
logger.debug(f"Token data: {token_data}")
access_token = token_data.get("access_token")
return access_token
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
async def shared_oauth_client_credentials(oauth_callback_server): async def shared_oauth_client_credentials(oauth_callback_server):
""" """
@@ -991,67 +849,6 @@ async def playwright_oauth_token(
return access_token return access_token
# Alternative fixtures using Playwright token (for automated/CI testing)
@pytest.fixture(scope="session")
async def nc_oauth_client_playwright(
playwright_oauth_token: str,
) -> AsyncGenerator[NextcloudClient, Any]:
"""
Fixture to create a NextcloudClient instance using automated Playwright OAuth authentication.
This fixture uses headless browser automation and is suitable for CI/CD pipelines.
For interactive testing, use nc_oauth_client fixture instead.
"""
nextcloud_host = os.getenv("NEXTCLOUD_HOST")
username = os.getenv("NEXTCLOUD_USERNAME")
if not all([nextcloud_host, username]):
pytest.skip(
"Playwright 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 Playwright OAuth NextcloudClient: {e}")
pytest.fail(f"Failed to connect to Nextcloud with Playwright OAuth token: {e}")
finally:
await client.close()
@pytest.fixture(scope="session")
async def nc_mcp_oauth_client_playwright(
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 fixture uses headless browser automation and is suitable for CI/CD pipelines.
For interactive testing, use nc_mcp_oauth_client fixture instead.
"""
async for session in create_mcp_client_session(
url="http://127.0.0.1:8001/mcp",
token=playwright_oauth_token,
client_name="OAuth MCP (Playwright Alt)",
):
yield session
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
async def test_users_setup(nc_client: NextcloudClient): async def test_users_setup(nc_client: NextcloudClient):
""" """
+2 -2
View File
@@ -38,11 +38,11 @@ async def test_mcp_oauth_tool_execution(nc_mcp_oauth_client):
) )
async def test_mcp_oauth_client_with_playwright(nc_mcp_oauth_client_playwright): async def test_mcp_oauth_client_with_playwright(nc_mcp_oauth_client):
"""Test that MCP OAuth client via Playwright can execute tools.""" """Test that MCP OAuth client via Playwright can execute tools."""
# Test: Execute the 'nc_notes_search_notes' tool # Test: Execute the 'nc_notes_search_notes' tool
result = await nc_mcp_oauth_client_playwright.call_tool( result = await nc_mcp_oauth_client.call_tool(
"nc_notes_search_notes", arguments={"query": ""} "nc_notes_search_notes", arguments={"query": ""}
) )
Generated
+5 -5
View File
@@ -593,7 +593,7 @@ wheels = [
[[package]] [[package]]
name = "mcp" name = "mcp"
version = "1.17.0" version = "1.18.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "anyio" }, { name = "anyio" },
@@ -608,9 +608,9 @@ dependencies = [
{ name = "starlette" }, { name = "starlette" },
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/5a/79/5724a540df19e192e8606c543cdcf162de8eb435077520cca150f7365ec0/mcp-1.17.0.tar.gz", hash = "sha256:1b57fabf3203240ccc48e39859faf3ae1ccb0b571ff798bbedae800c73c6df90", size = 477951, upload-time = "2025-10-10T12:16:44.519Z" } sdist = { url = "https://files.pythonhosted.org/packages/1a/e0/fe34ce16ea2bacce489ab859abd1b47ae28b438c3ef60b9c5eee6c02592f/mcp-1.18.0.tar.gz", hash = "sha256:aa278c44b1efc0a297f53b68df865b988e52dd08182d702019edcf33a8e109f6", size = 482926, upload-time = "2025-10-16T19:19:55.125Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/1c/72/3751feae343a5ad07959df713907b5c3fbaed269d697a14b0c449080cf2e/mcp-1.17.0-py3-none-any.whl", hash = "sha256:0660ef275cada7a545af154db3082f176cf1d2681d5e35ae63e014faf0a35d40", size = 167737, upload-time = "2025-10-10T12:16:42.863Z" }, { url = "https://files.pythonhosted.org/packages/1b/44/f5970e3e899803823826283a70b6003afd46f28e082544407e24575eccd3/mcp-1.18.0-py3-none-any.whl", hash = "sha256:42f10c270de18e7892fdf9da259029120b1ea23964ff688248c69db9d72b1d0a", size = 168762, upload-time = "2025-10-16T19:19:53.2Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -630,7 +630,7 @@ wheels = [
[[package]] [[package]]
name = "nextcloud-mcp-server" name = "nextcloud-mcp-server"
version = "0.14.2" version = "0.14.3"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },
@@ -659,7 +659,7 @@ requires-dist = [
{ name = "click", specifier = ">=8.1.8" }, { name = "click", specifier = ">=8.1.8" },
{ name = "httpx", specifier = ">=0.28.1,<0.29.0" }, { name = "httpx", specifier = ">=0.28.1,<0.29.0" },
{ name = "icalendar", specifier = ">=6.0.0,<7.0.0" }, { name = "icalendar", specifier = ">=6.0.0,<7.0.0" },
{ name = "mcp", extras = ["cli"], specifier = ">=1.17,<1.18" }, { name = "mcp", extras = ["cli"], specifier = ">=1.18,<1.19" },
{ name = "pillow", specifier = ">=12.0.0,<12.1.0" }, { name = "pillow", specifier = ">=12.0.0,<12.1.0" },
{ name = "pydantic", specifier = ">=2.11.4" }, { name = "pydantic", specifier = ">=2.11.4" },
{ name = "pythonvcard4", specifier = ">=0.2.0" }, { name = "pythonvcard4", specifier = ">=0.2.0" },