Skip to main content
Glama
cbcoutinho

Nextcloud MCP Server

by cbcoutinho
test_nc_php_app_oauth.py13.9 kB
"""Test OAuth authorization flow for Nextcloud PHP app (astrolabe). Tests the complete PKCE OAuth flow from the NC PHP app perspective: 1. User navigates to personal settings 2. Clicks "Authorize Access" button 3. Completes OAuth authorization via Nextcloud OIDC app 4. Token is stored encrypted in Nextcloud database 5. App can use token to call MCP management API This tests the architecture from ADR-018 where the NC PHP app uses OAuth PKCE (public client) to obtain tokens from Nextcloud's OIDC app. """ import logging import os import httpx import pytest logger = logging.getLogger(__name__) pytestmark = [pytest.mark.integration, pytest.mark.oauth] @pytest.fixture(scope="module") def nextcloud_credentials(): """Get Nextcloud credentials from environment.""" return { "host": os.getenv("NEXTCLOUD_HOST", "http://localhost:8080"), "username": os.getenv("NEXTCLOUD_USERNAME", "admin"), "password": os.getenv("NEXTCLOUD_PASSWORD", "admin"), } @pytest.fixture(scope="module") async def nc_admin_http_client(nextcloud_credentials): """HTTP client authenticated as admin user for NC API calls.""" async with httpx.AsyncClient( base_url=nextcloud_credentials["host"], auth=(nextcloud_credentials["username"], nextcloud_credentials["password"]), timeout=30.0, ) as client: yield client @pytest.fixture(scope="module") async def authorized_nc_session(browser, nextcloud_credentials): """Module-scoped fixture that logs in and authorizes the NC PHP app once. This fixture: 1. Creates a browser context 2. Logs in to Nextcloud 3. Authorizes the MCP Server UI app (if not already authorized) 4. Returns the page for use in all tests The authorization is done once and reused for all tests in this module. """ host = nextcloud_credentials["host"] username = nextcloud_credentials["username"] password = nextcloud_credentials["password"] logger.info("Setting up module-scoped authorized NC session...") # Create browser context that persists for module duration context = await browser.new_context() page = await context.new_page() # Enable console message logging page.on( "console", lambda msg: logger.debug(f"Browser console [{msg.type}]: {msg.text}") ) page.on("pageerror", lambda err: logger.error(f"Browser page error: {err}")) try: # Step 1: Login to Nextcloud logger.info(f"Logging in to Nextcloud as {username}...") await page.goto(f"{host}/login") # Fill login form await page.fill('input[name="user"]', username) await page.fill('input[name="password"]', password) await page.click('button[type="submit"]') # Wait for login to complete (dashboard loads) await page.wait_for_url(f"{host}/apps/dashboard/", timeout=10000) logger.info("✓ Logged in successfully") # Step 2: Navigate to personal MCP settings logger.info("Navigating to personal MCP settings...") await page.goto(f"{host}/settings/user/mcp") await page.wait_for_load_state("networkidle") page_content = await page.content() # Step 3: Check if authorization is needed if "Authorize Access" in page_content or "authorize" in page_content.lower(): logger.info("User not authorized yet - initiating OAuth flow...") # Click "Authorize Access" button authorize_selectors = [ 'button:has-text("Authorize")', 'a:has-text("Authorize")', '[href*="oauth/authorize"]', 'button:has-text("Connect")', ] clicked = False for selector in authorize_selectors: try: await page.click(selector, timeout=2000) clicked = True logger.info(f"✓ Clicked authorize button (selector: {selector})") break except Exception: continue if not clicked: screenshot_path = "/tmp/nc-php-app-settings.png" await page.screenshot(path=screenshot_path) pytest.fail( f"Could not find authorize button. Screenshot: {screenshot_path}" ) # Wait for page to load after clicking await page.wait_for_load_state("networkidle", timeout=10000) current_url = page.url # Handle OAuth consent if needed if "/apps/oidc/authorize" in current_url: logger.info("On OIDC authorization page - granting consent...") consent_selectors = [ 'button:has-text("Allow")', 'button:has-text("Authorize")', 'input[type="submit"][value="Allow"]', 'button[type="submit"]', ] for selector in consent_selectors: try: await page.click(selector, timeout=2000) logger.info(f"✓ Clicked consent button (selector: {selector})") break except Exception: continue # Wait for redirect back to settings await page.wait_for_url(f"{host}/settings/user/mcp", timeout=15000) await page.wait_for_load_state("networkidle") logger.info("✓ OAuth authorization completed") else: logger.info("User already authorized") # Return the page and context info for tests yield { "page": page, "context": context, "host": host, "username": username, } finally: # Cleanup at module end logger.info("Closing authorized NC session...") await context.close() class TestNcPhpAppOAuth: """Test suite for NC PHP app OAuth integration.""" async def test_authorization_completed(self, authorized_nc_session): """Verify OAuth authorization was successful. This test verifies the settings page shows the user is connected after the module-scoped authorization fixture runs. """ page = authorized_nc_session["page"] host = authorized_nc_session["host"] # Navigate to settings (may already be there) await page.goto(f"{host}/settings/user/mcp") await page.wait_for_load_state("networkidle") page_content = await page.content() # Look for indicators that authorization succeeded success_indicators = [ "Connected", "Disconnect", "Server Connection", "Session Information", "MCP Server", ] has_success_indicator = any( indicator in page_content for indicator in success_indicators ) if not has_success_indicator: screenshot_path = "/tmp/nc-php-app-auth-check.png" await page.screenshot(path=screenshot_path) logger.error(f"Authorization check failed. Screenshot: {screenshot_path}") assert has_success_indicator, "Settings page should show user is authorized" logger.info("✓ Authorization verification passed") async def test_token_storage_and_retrieval(self, authorized_nc_session): """Test that tokens are properly stored and can be retrieved. Verifies the settings page displays session information, indicating the token was stored and retrieved successfully. """ page = authorized_nc_session["page"] host = authorized_nc_session["host"] await page.goto(f"{host}/settings/user/mcp") await page.wait_for_load_state("networkidle") page_content = await page.content() # Debug: take screenshot and log content excerpt screenshot_path = "/tmp/nc-php-app-token-test.png" await page.screenshot(path=screenshot_path) logger.info(f"Screenshot saved: {screenshot_path}") logger.info(f"Page content excerpt: {page_content[:1000]}") # Verify session information is visible - these are the actual labels from template session_indicators = [ "Server Connection", "Session Information", "Connection Management", "MCP Server", ] found_indicators = [ind for ind in session_indicators if ind in page_content] assert len(found_indicators) >= 2, ( f"Expected session info on page. Found: {found_indicators}. Check {screenshot_path}" ) logger.info(f"✓ Token retrieval verified - found: {found_indicators}") async def test_management_api_access( self, authorized_nc_session, nc_admin_http_client ): """Test that the NC PHP app can access MCP server management API. Verifies the settings page successfully fetched data from the MCP server's management API endpoints. """ page = authorized_nc_session["page"] host = authorized_nc_session["host"] # Check personal settings page shows server status await page.goto(f"{host}/settings/user/mcp") await page.wait_for_load_state("networkidle") page_content = await page.content() # Look for data that comes from management API or template structure api_indicators = [ "Server Connection", # Section header "Server URL", # Server info "Connection Management", # Connection section "Vector Visualization", # Vector sync section ] found_api_data = [ind for ind in api_indicators if ind in page_content] assert len(found_api_data) >= 1, ( f"Expected management API data on page. Found: {found_api_data}" ) logger.info(f"✓ Management API access verified - found: {found_api_data}") async def test_admin_settings_page(self, authorized_nc_session): """Test that admin settings page loads and displays server info. The admin page should show server status from the management API. """ page = authorized_nc_session["page"] host = authorized_nc_session["host"] await page.goto(f"{host}/settings/admin/mcp") await page.wait_for_load_state("networkidle") page_content = await page.content() # Admin page should show server status admin_indicators = [ "MCP Server", "Server Status", "Version", ] found_indicators = [ind for ind in admin_indicators if ind in page_content] # Admin page should at least show the MCP Server header assert "MCP Server" in page_content or "mcp" in page_content.lower(), ( "Admin settings page should show MCP Server section" ) logger.info(f"✓ Admin settings page verified - found: {found_indicators}") class TestNcPhpAppDisconnect: """Test suite for NC PHP app disconnect functionality. Note: These tests are run separately and may modify the authorization state. They should run after the main OAuth tests. """ @pytest.mark.skip(reason="Disconnect test modifies state - run manually if needed") async def test_disconnect_flow(self, browser, nextcloud_credentials): """Test that users can disconnect (revoke) their authorization. This test: 1. Logs in fresh (separate from authorized_nc_session) 2. Verifies user is authorized 3. Clicks "Disconnect" button 4. Verifies user is no longer authorized Skipped by default as it modifies authorization state. """ host = nextcloud_credentials["host"] username = nextcloud_credentials["username"] password = nextcloud_credentials["password"] context = await browser.new_context() page = await context.new_page() try: # Login await page.goto(f"{host}/login") await page.fill('input[name="user"]', username) await page.fill('input[name="password"]', password) await page.click('button[type="submit"]') await page.wait_for_url(f"{host}/apps/dashboard/", timeout=10000) # Navigate to personal settings await page.goto(f"{host}/settings/user/mcp") await page.wait_for_load_state("networkidle") page_content = await page.content() # Check if user is authorized if "Disconnect" not in page_content: pytest.skip("User not authorized - cannot test disconnect") # Click disconnect button disconnect_selectors = [ 'button:has-text("Disconnect")', 'form[action*="disconnect"] button', "#mcp-disconnect-button", ] for selector in disconnect_selectors: try: # Handle confirmation dialog page.on("dialog", lambda dialog: dialog.accept()) await page.click(selector, timeout=2000) logger.info(f"✓ Clicked disconnect button (selector: {selector})") break except Exception: continue # Wait for page reload await page.wait_for_load_state("networkidle") # Verify we're back to "Authorize Access" state page_content = await page.content() assert "Authorize" in page_content, ( "Settings page should show 'Authorize Access' after disconnect" ) logger.info("✓ Disconnect flow test passed") finally: await context.close()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cbcoutinho/nextcloud-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server