From 51d1f075f5da2d338f8bc072ae2dfa451ed0abd0 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Thu, 16 Oct 2025 19:46:29 +0200 Subject: [PATCH] test: Remove duplicated/interactive testing fixtures All integration tests now run without interactive browser usage, simplifying CI and testing infrastructure --- CLAUDE.md | 55 +++---- tests/client/test_oauth_interactive.py | 41 ----- tests/client/test_oauth_playwright.py | 6 +- tests/conftest.py | 207 +------------------------ tests/server/test_mcp_oauth.py | 4 +- 5 files changed, 28 insertions(+), 285 deletions(-) delete mode 100644 tests/client/test_oauth_interactive.py diff --git a/CLAUDE.md b/CLAUDE.md index b579afd..507a9fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -132,47 +132,35 @@ 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 **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):** -- **Default fixtures**: `nc_oauth_client`, `nc_mcp_oauth_client` use Playwright automation -- Uses Playwright headless browser automation to complete OAuth flow programmatically +**OAuth Testing Setup:** +- **Main fixtures**: `nc_oauth_client`, `nc_mcp_oauth_client` - Use Playwright automation - **Shared OAuth Client**: All test users authenticate using a single OAuth client - Stored in `.nextcloud_oauth_shared_test_client.json` - Matches production MCP server behavior - Each user gets their own unique access token - - Implementation: `shared_oauth_client_credentials` fixture in `tests/conftest.py:812` -- All Playwright fixtures: `playwright_oauth_token`, `nc_oauth_client`, `nc_mcp_oauth_client`, `nc_oauth_client_playwright`, `nc_mcp_oauth_client_playwright` -- Multi-user fixtures: `alice_oauth_token`, `bob_oauth_token`, `charlie_oauth_token`, `diana_oauth_token` -- Requires: `NEXTCLOUD_HOST`, `NEXTCLOUD_USERNAME`, `NEXTCLOUD_PASSWORD` environment variables + - Implementation: `shared_oauth_client_credentials` fixture in `tests/conftest.py` +- **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` +- **Requirements**: `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 all OAuth tests with automated Playwright flow using Firefox - uv run pytest tests/server/test_oauth*.py --browser firefox -v +- **Playwright configuration**: Use pytest CLI args like `--browser firefox --headed` to customize +- **Install browsers**: `uv run playwright install firefox` (or `chromium`, `webkit`) - # Run specific Playwright tests with visible browser for debugging - uv run pytest tests/server/test_mcp_oauth.py --browser firefox --headed -v +**Example Commands:** +```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) - uv run pytest tests/server/test_oauth*.py -v - ``` +# Run specific tests with visible browser for debugging +uv run pytest tests/server/test_mcp_oauth.py --browser firefox --headed -v -**Interactive Testing (Manual browser login):** -- Opens system browser and waits for manual login/authorization -- 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 - ``` +# Run with Chromium (default) +uv run pytest tests/server/test_oauth*.py -v +``` -**Test Environment Setup:** +**Test Environment:** - **Two MCP server containers are available:** - `mcp` (port 8000): Uses basic auth with admin credentials - for most 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` - OAuth client credentials cached in `.nextcloud_oauth_shared_test_client.json` -**CI/CD Considerations:** -- Interactive OAuth tests are automatically skipped when `GITHUB_ACTIONS` environment variable is set -- Automated Playwright tests will run in CI/CD environments +**CI/CD Notes:** +- Playwright tests run in CI/CD environments - Use Firefox browser in CI: `--browser firefox` (Chromium may have issues with localhost redirects) ### Configuration Files diff --git a/tests/client/test_oauth_interactive.py b/tests/client/test_oauth_interactive.py deleted file mode 100644 index e107b10..0000000 --- a/tests/client/test_oauth_interactive.py +++ /dev/null @@ -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}") diff --git a/tests/client/test_oauth_playwright.py b/tests/client/test_oauth_playwright.py index 989f325..b127cf3 100644 --- a/tests/client/test_oauth_playwright.py +++ b/tests/client/test_oauth_playwright.py @@ -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 1: Check capabilities - capabilities = await nc_oauth_client_playwright.capabilities() + capabilities = await nc_oauth_client.capabilities() assert capabilities is not None logger.info("OAuth client (Playwright) successfully fetched capabilities") # 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) logger.info(f"OAuth client (Playwright) successfully listed {len(notes)} notes") diff --git a/tests/conftest.py b/tests/conftest.py index 7352d4c..3e898cc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -167,27 +167,6 @@ async def nc_mcp_client() -> AsyncGenerator[ClientSession, Any]: 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") async def nc_mcp_oauth_client( 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. 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. + Uses headless browser automation suitable for CI/CD. """ async for session in create_mcp_client_session( 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}") -@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") 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. + Uses headless browser automation suitable for CI/CD. """ nextcloud_host = os.getenv("NEXTCLOUD_HOST") username = os.getenv("NEXTCLOUD_USERNAME") @@ -689,84 +625,6 @@ def oauth_callback_server(): 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") async def shared_oauth_client_credentials(oauth_callback_server): """ @@ -991,67 +849,6 @@ async def playwright_oauth_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") async def test_users_setup(nc_client: NextcloudClient): """ diff --git a/tests/server/test_mcp_oauth.py b/tests/server/test_mcp_oauth.py index 839e098..ad3b09b 100644 --- a/tests/server/test_mcp_oauth.py +++ b/tests/server/test_mcp_oauth.py @@ -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: 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": ""} )