Files
Chris Coutinho f4759e424d feat: add webhook management UI and BeforeNodeDeletedEvent support
Added comprehensive webhook management capabilities including:

Webhook Client & API:
- Added WebhooksClient for Nextcloud webhooks API integration
- Create, list, update, and delete webhooks programmatically
- Support for event filters in webhook registration

Webhook Presets:
- Added preset system for common webhook configurations
- notes_sync: BeforeNodeDeletedEvent for Notes file operations
- calendar_sync: Calendar events (create, update, delete)
- deck_sync: Deck card operations
- files_sync: File system changes
- forms_sync: Form submissions (conditional)
- Filter presets by installed apps

Admin UI:
- Added multi-pane app view with tabs (User Info, Vector Sync, Webhooks)
- Webhooks tab for admin users only
- Enable/disable preset webhooks via UI
- View currently registered webhooks
- Uses htmx for dynamic loading and Alpine.js for tab state
- Admin permission checking via OCS API

CLI Improvements:
- Refactored CLI to separate module (cli.py)
- Updated entry point in pyproject.toml

BeforeNodeDeletedEvent Fix:
- Updated ADR-010 to document NodeDeletedEvent issue
- BeforeNodeDeletedEvent includes node.id before deletion
- NodeDeletedEvent lacks node.id (file already deleted)
- Implemented per Nextcloud maintainer recommendation

Testing:
- Added comprehensive webhook client tests
- Added webhook preset filtering tests
- Added admin permission tests

Configuration:
- Updated docker-compose.yml Qdrant settings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 20:35:08 +01:00

113 lines
3.6 KiB
Python

"""Unit tests for permission checking."""
import pytest
from httpx import AsyncClient
from nextcloud_mcp_server.auth.permissions import is_nextcloud_admin
from nextcloud_mcp_server.client.users import UsersClient
@pytest.fixture
def mock_request(mocker):
"""Create a mock Starlette request."""
request = mocker.Mock()
request.user = mocker.Mock()
request.user.display_name = "testuser"
return request
@pytest.fixture
def mock_http_client(mocker):
"""Create a mock HTTP client."""
return mocker.AsyncMock(spec=AsyncClient)
@pytest.mark.unit
async def test_is_nextcloud_admin_true(mock_request, mock_http_client, mocker):
"""Test checking if user is admin (admin group membership)."""
# Mock the get_user_groups method to return admin group
mock_get_user_groups = mocker.patch.object(
UsersClient, "get_user_groups", return_value=["admin", "users"]
)
is_admin = await is_nextcloud_admin(mock_request, mock_http_client)
assert is_admin is True
mock_get_user_groups.assert_called_once_with("testuser")
@pytest.mark.unit
async def test_is_nextcloud_admin_false(mock_request, mock_http_client, mocker):
"""Test checking if user is not admin (no admin group membership)."""
# Mock the get_user_groups method to return no admin group
mock_get_user_groups = mocker.patch.object(
UsersClient, "get_user_groups", return_value=["users", "editors"]
)
is_admin = await is_nextcloud_admin(mock_request, mock_http_client)
assert is_admin is False
mock_get_user_groups.assert_called_once_with("testuser")
@pytest.mark.unit
async def test_is_nextcloud_admin_empty_groups(mock_request, mock_http_client, mocker):
"""Test checking admin status when user has no groups."""
# Mock the get_user_groups method to return empty list
mock_get_user_groups = mocker.patch.object(
UsersClient, "get_user_groups", return_value=[]
)
is_admin = await is_nextcloud_admin(mock_request, mock_http_client)
assert is_admin is False
mock_get_user_groups.assert_called_once_with("testuser")
@pytest.mark.unit
async def test_is_nextcloud_admin_no_username(mock_request, mock_http_client, mocker):
"""Test checking admin status when username is missing."""
# Set username to None
mock_request.user.display_name = None
mock_get_user_groups = mocker.patch.object(UsersClient, "get_user_groups")
is_admin = await is_nextcloud_admin(mock_request, mock_http_client)
assert is_admin is False
# Ensure get_user_groups was not called
mock_get_user_groups.assert_not_called()
@pytest.mark.unit
async def test_is_nextcloud_admin_api_error(mock_request, mock_http_client, mocker):
"""Test checking admin status when API call fails."""
# Mock the get_user_groups method to raise an exception
mock_get_user_groups = mocker.patch.object(
UsersClient,
"get_user_groups",
side_effect=Exception("API error"),
)
is_admin = await is_nextcloud_admin(mock_request, mock_http_client)
assert is_admin is False
mock_get_user_groups.assert_called_once_with("testuser")
@pytest.mark.unit
async def test_is_nextcloud_admin_case_sensitive(
mock_request, mock_http_client, mocker
):
"""Test that admin group check is case-sensitive."""
# Mock with "Admin" (capital A) instead of "admin"
mock_get_user_groups = mocker.patch.object(
UsersClient, "get_user_groups", return_value=["Admin", "users"]
)
is_admin = await is_nextcloud_admin(mock_request, mock_http_client)
# Should be False because Nextcloud uses lowercase "admin"
assert is_admin is False
mock_get_user_groups.assert_called_once_with("testuser")