140 lines
5.8 KiB
Python
140 lines
5.8 KiB
Python
"""Integration test for Astrolabe's "Enable Semantic Search" OAuth flow on
|
|
the `mcp-login-flow` profile.
|
|
|
|
Cross-system interface test. Brings together Astrolabe (Nextcloud PHP app
|
|
installed at container start by ``app-hooks/post-installation``) with the
|
|
``mcp-login-flow`` MCP server over OAuth + the management API. Mirrors
|
|
the production-shaped flow that PR #773's recent
|
|
`ALLOWED_MGMT_CLIENT` ↔ `astrolabeMcpClientOAuth00000000000` drift was
|
|
masking — every management API call from Astrolabe (e.g.
|
|
``/api/v1/users/admin/session``) was returning 401 because the
|
|
real-deployment client id was not in the test-fixture allowlist, so the
|
|
Astrolabe settings page never updated to reflect a successful
|
|
authorization.
|
|
|
|
This test is **regression coverage** for that class of drift. If the
|
|
Astrolabe client id ever falls out of `mcp-login-flow`'s
|
|
``ALLOWED_MGMT_CLIENT`` again, the post-redirect assertions here will
|
|
fail because the page state stays on ``oauth-required.php``.
|
|
|
|
Requires the login-flow stack to be running:
|
|
|
|
MCP_SERVER_URL=http://mcp-login-flow:8004 \\
|
|
docker compose --profile login-flow up -d app db mcp-login-flow
|
|
|
|
The ``app-hooks/before-starting/26-configure-astrolabe-oauth.sh`` hook
|
|
creates the OAuth client with the production-shaped id
|
|
``astrolabeMcpClientOAuth00000000000`` automatically when
|
|
``MCP_SERVER_URL`` is set, so no fixture-level OIDC client creation is
|
|
needed here.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import re
|
|
|
|
import pytest
|
|
from playwright.async_api import Page
|
|
|
|
# Reuse helpers from the multi-user-basic Astrolabe test for login + nav.
|
|
from tests.integration.test_astrolabe_multi_user_background_sync import (
|
|
login_to_nextcloud,
|
|
navigate_to_astrolabe_settings,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
pytestmark = [pytest.mark.integration, pytest.mark.login_flow]
|
|
|
|
NEXTCLOUD_URL = "http://localhost:8080"
|
|
ASTROLABE_SETTINGS_URL = f"{NEXTCLOUD_URL}/settings/user/astrolabe"
|
|
|
|
|
|
async def _click_enable_semantic_search(page: Page) -> None:
|
|
"""Click the "Enable Semantic Search" OAuth link on the
|
|
``oauth-required.php`` template that login-flow mode renders to a
|
|
not-yet-authorized user.
|
|
|
|
Astrolabe's own e2e helper (``third_party/astrolabe/tests/e2e/helpers/
|
|
authorize.ts``) targets the same link by accessible name.
|
|
"""
|
|
enable_link = page.get_by_role("link", name="Enable Semantic Search")
|
|
await enable_link.wait_for(state="visible", timeout=10_000)
|
|
logger.info("Clicking 'Enable Semantic Search' OAuth link")
|
|
await enable_link.click()
|
|
|
|
|
|
async def _grant_oidc_consent(page: Page) -> None:
|
|
"""Click "Allow" on the Nextcloud OIDC consent screen, if shown.
|
|
|
|
Nextcloud may auto-redirect for already-trusted clients, in which
|
|
case the consent button never appears — that's not an error.
|
|
"""
|
|
allow_button = page.get_by_role("button", name=re.compile(r"^allow$", re.I))
|
|
try:
|
|
await allow_button.wait_for(state="visible", timeout=10_000)
|
|
logger.info("Clicking 'Allow' on OIDC consent")
|
|
await allow_button.click(force=True)
|
|
except Exception:
|
|
logger.info(
|
|
"OIDC consent screen not visible — assuming auto-grant for "
|
|
"already-trusted client"
|
|
)
|
|
|
|
|
|
@pytest.mark.timeout(180)
|
|
async def test_enable_semantic_search_completes_oauth_for_login_flow(browser):
|
|
"""Click the "Enable Semantic Search" link, grant consent, and assert
|
|
the post-redirect page reflects a completed authorization.
|
|
|
|
The success criterion is intentionally negative: after the OAuth
|
|
flow, the original "Enable Semantic Search" link must be gone. If
|
|
Astrolabe's management API call is rejected by the MCP server (HTTP
|
|
401, the original bug), the page falls back to the same
|
|
``oauth-required.php`` template and the link reappears — making this
|
|
test the canary for the drift class.
|
|
"""
|
|
admin_password = os.getenv("NEXTCLOUD_PASSWORD")
|
|
if admin_password is None:
|
|
raise RuntimeError("NEXTCLOUD_PASSWORD must be set")
|
|
page = await browser.new_page()
|
|
try:
|
|
await login_to_nextcloud(page, "admin", admin_password)
|
|
await navigate_to_astrolabe_settings(page)
|
|
|
|
# Sanity-check we're on the not-yet-authorized template.
|
|
enable_link = page.get_by_role("link", name="Enable Semantic Search")
|
|
if await enable_link.count() == 0:
|
|
pytest.skip(
|
|
"Astrolabe is already authorized for admin (oauth-required.php "
|
|
"not rendered). Reset by clearing the user's OAuth tokens "
|
|
"before re-running this test."
|
|
)
|
|
|
|
await _click_enable_semantic_search(page)
|
|
await _grant_oidc_consent(page)
|
|
|
|
# OAuth callback returns to /apps/astrolabe/oauth/callback then the
|
|
# controller redirects to /settings/user/astrolabe.
|
|
await page.wait_for_url(re.compile(r"/settings/user/astrolabe"), timeout=30_000)
|
|
await page.wait_for_load_state("networkidle", timeout=15_000)
|
|
|
|
# Regression assertion for the ALLOWED_MGMT_CLIENT drift bug:
|
|
# the page must have moved past oauth-required.php. If
|
|
# Astrolabe's management API call to /api/v1/users/{id}/session
|
|
# is rejected (401), the session lookup falls back to "no token",
|
|
# and the same oauth-required.php template re-renders with the
|
|
# link still present.
|
|
post_auth_count = await page.get_by_role(
|
|
"link", name="Enable Semantic Search"
|
|
).count()
|
|
assert post_auth_count == 0, (
|
|
"'Enable Semantic Search' link still visible after completing "
|
|
"OAuth flow — Astrolabe could not read the user's session from "
|
|
"the MCP server. Most likely cause: "
|
|
"`astrolabeMcpClientOAuth00000000000` missing from "
|
|
"`ALLOWED_MGMT_CLIENT` on `mcp-login-flow`."
|
|
)
|
|
finally:
|
|
await page.close()
|