refactor(api): split management.py into domain-focused modules

Split the monolithic management.py (1988 lines) into 4 focused modules:
- management.py: Server status, user sessions, shared helpers (~520 lines)
- passwords.py: App password provisioning for BasicAuth mode (~300 lines)
- webhooks.py: Webhook registration management (~290 lines)
- visualization.py: Search and PDF preview endpoints (~810 lines)

Backward compatibility maintained via __init__.py re-exports.
Updated test imports to use new module paths.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-01-26 21:28:18 +01:00
parent 61ce873411
commit 2e7774654b
8 changed files with 1674 additions and 1522 deletions
@@ -18,8 +18,8 @@ from starlette.applications import Starlette
from starlette.routing import Route
from starlette.testclient import TestClient
from nextcloud_mcp_server.api import management
from nextcloud_mcp_server.api.management import (
from nextcloud_mcp_server.api import passwords
from nextcloud_mcp_server.api.passwords import (
delete_app_password,
get_app_password_status,
provision_app_password,
@@ -32,9 +32,9 @@ pytestmark = pytest.mark.unit
@pytest.fixture(autouse=True)
def clear_rate_limit():
"""Clear rate limit state before each test."""
management._rate_limit_attempts.clear()
passwords._rate_limit_attempts.clear()
yield
management._rate_limit_attempts.clear()
passwords._rate_limit_attempts.clear()
@pytest.fixture
@@ -199,7 +199,7 @@ async def test_provision_app_password_success(temp_storage, mocker):
mock_client.__aexit__ = AsyncMock()
mocker.patch(
"nextcloud_mcp_server.api.management.httpx.AsyncClient",
"nextcloud_mcp_server.api.passwords.httpx.AsyncClient",
return_value=mock_client,
)
@@ -243,7 +243,7 @@ async def test_provision_app_password_nextcloud_validation_fails(mocker):
mock_client.__aexit__ = AsyncMock()
mocker.patch(
"nextcloud_mcp_server.api.management.httpx.AsyncClient",
"nextcloud_mcp_server.api.passwords.httpx.AsyncClient",
return_value=mock_client,
)
@@ -362,7 +362,7 @@ async def test_delete_app_password_success(temp_storage, mocker):
mock_client.__aexit__ = AsyncMock()
mocker.patch(
"nextcloud_mcp_server.api.management.httpx.AsyncClient",
"nextcloud_mcp_server.api.passwords.httpx.AsyncClient",
return_value=mock_client,
)
@@ -406,7 +406,7 @@ async def test_delete_app_password_not_found(temp_storage, mocker):
mock_client.__aexit__ = AsyncMock()
mocker.patch(
"nextcloud_mcp_server.api.management.httpx.AsyncClient",
"nextcloud_mcp_server.api.passwords.httpx.AsyncClient",
return_value=mock_client,
)
@@ -445,7 +445,7 @@ async def test_delete_app_password_invalid_credentials(mocker):
mock_client.__aexit__ = AsyncMock()
mocker.patch(
"nextcloud_mcp_server.api.management.httpx.AsyncClient",
"nextcloud_mcp_server.api.passwords.httpx.AsyncClient",
return_value=mock_client,
)
@@ -515,7 +515,7 @@ async def test_provision_app_password_rate_limiting(mocker):
mock_client.__aexit__ = AsyncMock()
mocker.patch(
"nextcloud_mcp_server.api.management.httpx.AsyncClient",
"nextcloud_mcp_server.api.passwords.httpx.AsyncClient",
return_value=mock_client,
)
@@ -574,7 +574,7 @@ async def test_rate_limiting_is_per_user(mocker):
mock_client.__aexit__ = AsyncMock()
mocker.patch(
"nextcloud_mcp_server.api.management.httpx.AsyncClient",
"nextcloud_mcp_server.api.passwords.httpx.AsyncClient",
return_value=mock_client,
)
@@ -16,7 +16,7 @@ from starlette.applications import Starlette
from starlette.routing import Route
from starlette.testclient import TestClient
from nextcloud_mcp_server.api.management import get_pdf_preview
from nextcloud_mcp_server.api.visualization import get_pdf_preview
pytestmark = pytest.mark.unit
@@ -68,12 +68,12 @@ class TestPdfPreviewParameterValidation:
"""Test that missing file_path parameter returns 400."""
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
):
@@ -93,12 +93,12 @@ class TestPdfPreviewParameterValidation:
"""Test that invalid page number (0 or negative) returns 400."""
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
):
@@ -126,12 +126,12 @@ class TestPdfPreviewParameterValidation:
"""Test that scale outside valid range returns 400."""
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
):
@@ -159,12 +159,12 @@ class TestPdfPreviewParameterValidation:
"""Test that non-numeric page parameter returns 400."""
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
):
@@ -186,7 +186,7 @@ class TestPdfPreviewAuthentication:
def test_unauthorized_without_token_returns_401(self):
"""Test that request without token returns 401."""
with patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
side_effect=Exception("Invalid token"),
):
@@ -201,7 +201,7 @@ class TestPdfPreviewAuthentication:
def test_unauthorized_with_invalid_token_returns_401(self):
"""Test that request with invalid token returns 401."""
with patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
side_effect=Exception("Token expired"),
):
@@ -235,12 +235,12 @@ class TestPdfPreviewRendering:
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
patch(
@@ -284,12 +284,12 @@ class TestPdfPreviewRendering:
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
patch(
@@ -324,12 +324,12 @@ class TestPdfPreviewRendering:
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
patch(
@@ -363,12 +363,12 @@ class TestPdfPreviewRendering:
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
patch(
@@ -407,12 +407,12 @@ class TestPdfPreviewEdgeCases:
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
patch(
@@ -438,12 +438,12 @@ class TestPdfPreviewEdgeCases:
"""Test handling when Nextcloud host is not configured."""
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
):
@@ -476,12 +476,12 @@ class TestPdfPreviewEdgeCases:
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
patch(
@@ -518,12 +518,12 @@ class TestPdfPreviewEdgeCases:
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
patch(
@@ -556,12 +556,12 @@ class TestPdfPreviewSecurityValidation:
"""Test that path traversal attempts are blocked with 400."""
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
):
@@ -605,12 +605,12 @@ class TestPdfPreviewSecurityValidation:
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
patch(
@@ -647,12 +647,12 @@ class TestPdfPreviewSecurityValidation:
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
patch(
@@ -691,12 +691,12 @@ class TestPdfPreviewSecurityValidation:
with (
patch(
"nextcloud_mcp_server.api.management.validate_token_and_get_user",
"nextcloud_mcp_server.api.visualization.validate_token_and_get_user",
new_callable=AsyncMock,
return_value=("testuser", True),
),
patch(
"nextcloud_mcp_server.api.management.extract_bearer_token",
"nextcloud_mcp_server.api.visualization.extract_bearer_token",
return_value="test-token",
),
patch(