Files
mcp-nextcloud/tests/unit/test_management_status_endpoint.py
T
Chris CoutinhoandClaude Opus 4.7 cafd318f36 refactor(config)!: rename OAUTH_SINGLE_AUDIENCE to LOGIN_FLOW, gate on ENABLE_LOGIN_FLOW
The AuthMode.OAUTH_SINGLE_AUDIENCE enum was a vestige of ADR-021's
original design where it co-existed with OAUTH_TOKEN_EXCHANGE. The
un-augmented OAuth bearer pass-through it represented relied on
Nextcloud-side patches to user_oidc (Bearer token validation on
non-OCS endpoints) that were never merged upstream (see
docs/authentication.md, docs/login-flow-v2.md). The working path —
mcp-login-flow profile — sets ENABLE_LOGIN_FLOW=true on top of this
mode so Login Flow v2 acquires per-user Nextcloud app passwords via
a browser flow. With OAUTH_TOKEN_EXCHANGE removed in 57303135, the
_AUDIENCE suffix in the Python name no longer disambiguates anything,
and the enum value diverged from the env-var spelling. ADR-022 (now
accepted) called for this rename as step 1 of consolidation.

- nextcloud_mcp_server/config_validators.py: rename enum to LOGIN_FLOW
  with value "login_flow". The mode_map key is now "login_flow"; the
  MODE_REQUIREMENTS entry requires `enable_login_flow=True`. Added a
  validation gate so MCP_DEPLOYMENT_MODE=login_flow without
  ENABLE_LOGIN_FLOW=true errors with a clear message pointing at
  ADR-022. Default auto-detection fallback returns LOGIN_FLOW.
- nextcloud_mcp_server/app.py: renamed three identifier references and
  switched the "Configuring MCP server for OAuth mode" log line to
  the uniform `mode.value` shape used by the other modes.
- nextcloud_mcp_server/api/management.py: renamed identifier in the
  /api/v1/status mapping. The user-visible "auth_mode": "oauth" string
  is preserved — that's a stable Astrolabe contract.
- nextcloud_mcp_server/config.py: updated Settings docstring.
- tests/unit/test_config_validators.py: renamed class
  TestOAuthSingleAudienceValidation → TestLoginFlowValidation,
  individual test methods, env-var strings; added enable_login_flow=True
  to fixtures expecting success; added a new test
  (test_login_flow_requires_enable_login_flow_flag) that exercises the
  validation gate.
- tests/unit/test_management_status_endpoint.py: renamed identifier.

BREAKING CHANGE: MCP_DEPLOYMENT_MODE=oauth_single_audience is no longer
accepted. Set MCP_DEPLOYMENT_MODE=login_flow (and keep
ENABLE_LOGIN_FLOW=true) for the same deployment. The un-augmented
OAuth path is no longer supported; if you previously ran the broken
path, you can either configure Login Flow v2 (recommended) or switch
to multi_user_basic / single_user_basic.

Dead-code pruning of `oauth_enabled and not enable_login_flow`
branches in app.py (lifespan, background sync) is deferred to a
separate follow-up PR per the consolidation plan in ADR-022.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:34:13 +02:00

345 lines
12 KiB
Python

"""
Unit tests for Management API status endpoint.
Tests the /api/v1/status endpoint focusing on:
- OIDC config availability in different auth modes
- Hybrid mode (multi_user_basic + enable_offline_access) returning OIDC config
- OAuth mode returning OIDC config
- Non-OAuth modes NOT returning OIDC config
"""
from unittest.mock import MagicMock, patch
import pytest
from starlette.applications import Starlette
from starlette.routing import Route
from starlette.testclient import TestClient
from nextcloud_mcp_server.api.management import get_server_status
from nextcloud_mcp_server.config_validators import AuthMode
pytestmark = pytest.mark.unit
def create_test_app():
"""Create a test Starlette app with the status endpoint."""
return Starlette(
routes=[
Route("/api/v1/status", get_server_status, methods=["GET"]),
]
)
def create_mock_settings(
enable_multi_user_basic: bool = False,
enable_offline_access: bool = False,
oidc_discovery_url: str | None = None,
oidc_issuer: str | None = None,
vector_sync_enabled: bool = False,
nextcloud_url: str = "http://localhost",
mcp_client_id: str | None = None,
mcp_client_secret: str | None = None,
):
"""Create mock settings with specified auth configuration."""
settings = MagicMock()
settings.enable_multi_user_basic_auth = enable_multi_user_basic
settings.enable_offline_access = enable_offline_access
settings.oidc_discovery_url = oidc_discovery_url
settings.oidc_issuer = oidc_issuer
settings.vector_sync_enabled = vector_sync_enabled
settings.nextcloud_url = nextcloud_url
settings.mcp_client_id = mcp_client_id
settings.mcp_client_secret = mcp_client_secret
return settings
class TestStatusEndpointOidcConfig:
"""Tests for OIDC configuration in status endpoint."""
def test_hybrid_mode_returns_oidc_config(self):
"""Test that hybrid mode (multi_user_basic + offline_access) returns OIDC config."""
mock_settings = create_mock_settings(
enable_multi_user_basic=True,
enable_offline_access=True,
oidc_discovery_url="http://keycloak/.well-known/openid-configuration",
oidc_issuer="http://keycloak/realms/test",
)
# get_settings and detect_auth_mode are imported inside the function
with (
patch(
"nextcloud_mcp_server.api.management.get_settings",
return_value=mock_settings,
),
patch(
"nextcloud_mcp_server.api.management.detect_auth_mode",
return_value=AuthMode.MULTI_USER_BASIC,
),
):
app = create_test_app()
client = TestClient(app)
response = client.get("/api/v1/status")
assert response.status_code == 200
data = response.json()
# Verify auth mode
assert data["auth_mode"] == "multi_user_basic"
assert data["supports_app_passwords"] is True
# Verify OIDC config is present (key feature for hybrid mode)
assert "oidc" in data
assert (
data["oidc"]["discovery_url"]
== "http://keycloak/.well-known/openid-configuration"
)
assert data["oidc"]["issuer"] == "http://keycloak/realms/test"
def test_hybrid_mode_without_oidc_settings_no_oidc_key(self):
"""Test that hybrid mode without OIDC settings doesn't include empty oidc key."""
mock_settings = create_mock_settings(
enable_multi_user_basic=True,
enable_offline_access=True,
oidc_discovery_url=None,
oidc_issuer=None,
)
with (
patch(
"nextcloud_mcp_server.api.management.get_settings",
return_value=mock_settings,
),
patch(
"nextcloud_mcp_server.api.management.detect_auth_mode",
return_value=AuthMode.MULTI_USER_BASIC,
),
):
app = create_test_app()
client = TestClient(app)
response = client.get("/api/v1/status")
assert response.status_code == 200
data = response.json()
# OIDC key should NOT be present if no OIDC settings configured
assert "oidc" not in data
def test_multi_user_basic_without_offline_access_no_oidc(self):
"""Test that multi_user_basic WITHOUT offline_access doesn't return OIDC config."""
mock_settings = create_mock_settings(
enable_multi_user_basic=True,
enable_offline_access=False, # Key difference: no offline access
oidc_discovery_url="http://keycloak/.well-known/openid-configuration",
oidc_issuer="http://keycloak/realms/test",
)
with (
patch(
"nextcloud_mcp_server.api.management.get_settings",
return_value=mock_settings,
),
patch(
"nextcloud_mcp_server.api.management.detect_auth_mode",
return_value=AuthMode.MULTI_USER_BASIC,
),
):
app = create_test_app()
client = TestClient(app)
response = client.get("/api/v1/status")
assert response.status_code == 200
data = response.json()
# Verify auth mode
assert data["auth_mode"] == "multi_user_basic"
assert data["supports_app_passwords"] is False
# OIDC config should NOT be present (not hybrid mode)
assert "oidc" not in data
def test_oauth_mode_returns_oidc_config(self):
"""Test that OAuth mode returns OIDC config."""
mock_settings = create_mock_settings(
enable_multi_user_basic=False,
enable_offline_access=True,
oidc_discovery_url="http://nextcloud/.well-known/openid-configuration",
oidc_issuer="http://nextcloud",
)
with (
patch(
"nextcloud_mcp_server.api.management.get_settings",
return_value=mock_settings,
),
patch(
"nextcloud_mcp_server.api.management.detect_auth_mode",
return_value=AuthMode.LOGIN_FLOW,
),
):
app = create_test_app()
client = TestClient(app)
response = client.get("/api/v1/status")
assert response.status_code == 200
data = response.json()
# Verify auth mode
assert data["auth_mode"] == "oauth"
# Verify OIDC config is present
assert "oidc" in data
assert (
data["oidc"]["discovery_url"]
== "http://nextcloud/.well-known/openid-configuration"
)
def test_single_user_basic_no_oidc(self):
"""Test that single-user BasicAuth mode doesn't return OIDC config."""
mock_settings = create_mock_settings(
enable_multi_user_basic=False,
enable_offline_access=False,
oidc_discovery_url="http://keycloak/.well-known/openid-configuration",
oidc_issuer="http://keycloak/realms/test",
)
with (
patch(
"nextcloud_mcp_server.api.management.get_settings",
return_value=mock_settings,
),
patch(
"nextcloud_mcp_server.api.management.detect_auth_mode",
return_value=AuthMode.SINGLE_USER_BASIC,
),
):
app = create_test_app()
client = TestClient(app)
response = client.get("/api/v1/status")
assert response.status_code == 200
data = response.json()
# Verify auth mode
assert data["auth_mode"] == "basic"
# OIDC config should NOT be present
assert "oidc" not in data
# supports_app_passwords should NOT be present (only for multi_user_basic)
assert "supports_app_passwords" not in data
def test_oidc_partial_config_only_discovery_url(self):
"""Test OIDC config with only discovery URL set."""
mock_settings = create_mock_settings(
enable_multi_user_basic=True,
enable_offline_access=True,
oidc_discovery_url="http://keycloak/.well-known/openid-configuration",
oidc_issuer=None, # Only discovery URL
)
with (
patch(
"nextcloud_mcp_server.api.management.get_settings",
return_value=mock_settings,
),
patch(
"nextcloud_mcp_server.api.management.detect_auth_mode",
return_value=AuthMode.MULTI_USER_BASIC,
),
):
app = create_test_app()
client = TestClient(app)
response = client.get("/api/v1/status")
assert response.status_code == 200
data = response.json()
assert "oidc" in data
assert (
data["oidc"]["discovery_url"]
== "http://keycloak/.well-known/openid-configuration"
)
assert "issuer" not in data["oidc"]
def test_oidc_partial_config_only_issuer(self):
"""Test OIDC config with only issuer set."""
mock_settings = create_mock_settings(
enable_multi_user_basic=True,
enable_offline_access=True,
oidc_discovery_url=None, # Only issuer
oidc_issuer="http://keycloak/realms/test",
)
with (
patch(
"nextcloud_mcp_server.api.management.get_settings",
return_value=mock_settings,
),
patch(
"nextcloud_mcp_server.api.management.detect_auth_mode",
return_value=AuthMode.MULTI_USER_BASIC,
),
):
app = create_test_app()
client = TestClient(app)
response = client.get("/api/v1/status")
assert response.status_code == 200
data = response.json()
assert "oidc" in data
assert "discovery_url" not in data["oidc"]
assert data["oidc"]["issuer"] == "http://keycloak/realms/test"
class TestStatusEndpointBasicResponse:
"""Tests for basic status endpoint response fields."""
def test_status_includes_version(self):
"""Test that status endpoint includes version."""
mock_settings = create_mock_settings()
with (
patch(
"nextcloud_mcp_server.api.management.get_settings",
return_value=mock_settings,
),
patch(
"nextcloud_mcp_server.api.management.detect_auth_mode",
return_value=AuthMode.SINGLE_USER_BASIC,
),
):
app = create_test_app()
client = TestClient(app)
response = client.get("/api/v1/status")
assert response.status_code == 200
data = response.json()
assert "version" in data
assert "uptime_seconds" in data
assert "management_api_version" in data
assert data["management_api_version"] == "1.0"
def test_status_includes_vector_sync_enabled(self):
"""Test that status endpoint includes vector_sync_enabled."""
mock_settings = create_mock_settings(vector_sync_enabled=True)
with (
patch(
"nextcloud_mcp_server.api.management.get_settings",
return_value=mock_settings,
),
patch(
"nextcloud_mcp_server.api.management.detect_auth_mode",
return_value=AuthMode.SINGLE_USER_BASIC,
),
):
app = create_test_app()
client = TestClient(app)
response = client.get("/api/v1/status")
assert response.status_code == 200
data = response.json()
assert data["vector_sync_enabled"] is True