f4759e424d
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>
219 lines
6.8 KiB
Python
219 lines
6.8 KiB
Python
"""Unit tests for WebhooksClient."""
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
from nextcloud_mcp_server.client.webhooks import WebhooksClient
|
|
|
|
|
|
@pytest.fixture
|
|
def webhooks_client(mocker):
|
|
"""Create a WebhooksClient with mocked HTTP client."""
|
|
mock_http_client = mocker.AsyncMock(spec=AsyncClient)
|
|
return WebhooksClient(mock_http_client, "testuser")
|
|
|
|
|
|
@pytest.mark.unit
|
|
async def test_list_webhooks(webhooks_client, mocker):
|
|
"""Test listing registered webhooks."""
|
|
mock_response = mocker.Mock()
|
|
mock_response.json.return_value = {
|
|
"ocs": {
|
|
"data": [
|
|
{
|
|
"id": 1,
|
|
"uri": "http://example.com/webhook",
|
|
"event": "OCP\\Files\\Events\\Node\\NodeCreatedEvent",
|
|
"httpMethod": "POST",
|
|
},
|
|
{
|
|
"id": 2,
|
|
"uri": "http://example.com/webhook",
|
|
"event": "OCP\\Files\\Events\\Node\\NodeWrittenEvent",
|
|
"httpMethod": "POST",
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
mock_make_request = mocker.patch.object(
|
|
WebhooksClient, "_make_request", return_value=mock_response
|
|
)
|
|
|
|
webhooks = await webhooks_client.list_webhooks()
|
|
|
|
assert len(webhooks) == 2
|
|
assert webhooks[0]["id"] == 1
|
|
assert webhooks[0]["event"] == "OCP\\Files\\Events\\Node\\NodeCreatedEvent"
|
|
assert webhooks[1]["id"] == 2
|
|
|
|
mock_make_request.assert_called_once_with(
|
|
"GET",
|
|
"/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks",
|
|
headers={"OCS-APIRequest": "true", "Accept": "application/json"},
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
async def test_list_webhooks_empty(webhooks_client, mocker):
|
|
"""Test listing webhooks when none are registered."""
|
|
mock_response = mocker.Mock()
|
|
mock_response.json.return_value = {"ocs": {"data": []}}
|
|
|
|
mocker.patch.object(WebhooksClient, "_make_request", return_value=mock_response)
|
|
|
|
webhooks = await webhooks_client.list_webhooks()
|
|
|
|
assert webhooks == []
|
|
|
|
|
|
@pytest.mark.unit
|
|
async def test_create_webhook(webhooks_client, mocker):
|
|
"""Test creating a webhook registration."""
|
|
mock_response = mocker.Mock()
|
|
mock_response.json.return_value = {
|
|
"ocs": {
|
|
"data": {
|
|
"id": 123,
|
|
"uri": "http://example.com/webhook",
|
|
"event": "OCP\\Files\\Events\\Node\\NodeCreatedEvent",
|
|
"httpMethod": "POST",
|
|
"authMethod": "none",
|
|
}
|
|
}
|
|
}
|
|
|
|
mock_make_request = mocker.patch.object(
|
|
WebhooksClient, "_make_request", return_value=mock_response
|
|
)
|
|
|
|
webhook_data = await webhooks_client.create_webhook(
|
|
event="OCP\\Files\\Events\\Node\\NodeCreatedEvent",
|
|
uri="http://example.com/webhook",
|
|
)
|
|
|
|
assert webhook_data["id"] == 123
|
|
assert webhook_data["event"] == "OCP\\Files\\Events\\Node\\NodeCreatedEvent"
|
|
|
|
mock_make_request.assert_called_once()
|
|
call_args = mock_make_request.call_args
|
|
assert call_args[0][0] == "POST"
|
|
assert call_args[0][1] == "/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks"
|
|
|
|
|
|
@pytest.mark.unit
|
|
async def test_create_webhook_with_filter(webhooks_client, mocker):
|
|
"""Test creating a webhook with event filter."""
|
|
mock_response = mocker.Mock()
|
|
mock_response.json.return_value = {
|
|
"ocs": {
|
|
"data": {
|
|
"id": 124,
|
|
"uri": "http://example.com/webhook",
|
|
"event": "OCP\\Files\\Events\\Node\\NodeCreatedEvent",
|
|
"eventFilter": {"user.uid": "bob"},
|
|
}
|
|
}
|
|
}
|
|
|
|
mock_make_request = mocker.patch.object(
|
|
WebhooksClient, "_make_request", return_value=mock_response
|
|
)
|
|
|
|
webhook_data = await webhooks_client.create_webhook(
|
|
event="OCP\\Files\\Events\\Node\\NodeCreatedEvent",
|
|
uri="http://example.com/webhook",
|
|
event_filter={"user.uid": "bob"},
|
|
)
|
|
|
|
assert webhook_data["id"] == 124
|
|
assert webhook_data["eventFilter"] == {"user.uid": "bob"}
|
|
|
|
mock_make_request.assert_called_once()
|
|
call_args = mock_make_request.call_args
|
|
assert call_args[1]["json"]["eventFilter"] == {"user.uid": "bob"}
|
|
|
|
|
|
@pytest.mark.unit
|
|
async def test_create_webhook_with_auth_headers(webhooks_client, mocker):
|
|
"""Test creating a webhook with authentication headers."""
|
|
mock_response = mocker.Mock()
|
|
mock_response.json.return_value = {
|
|
"ocs": {
|
|
"data": {
|
|
"id": 125,
|
|
"uri": "http://example.com/webhook",
|
|
"event": "OCP\\Files\\Events\\Node\\NodeCreatedEvent",
|
|
"authMethod": "bearer",
|
|
}
|
|
}
|
|
}
|
|
|
|
mock_make_request = mocker.patch.object(
|
|
WebhooksClient, "_make_request", return_value=mock_response
|
|
)
|
|
|
|
webhook_data = await webhooks_client.create_webhook(
|
|
event="OCP\\Files\\Events\\Node\\NodeCreatedEvent",
|
|
uri="http://example.com/webhook",
|
|
auth_method="bearer",
|
|
headers={"Authorization": "Bearer secret-token"},
|
|
)
|
|
|
|
assert webhook_data["id"] == 125
|
|
assert webhook_data["authMethod"] == "bearer"
|
|
|
|
mock_make_request.assert_called_once()
|
|
call_args = mock_make_request.call_args
|
|
assert call_args[1]["json"]["authMethod"] == "bearer"
|
|
assert call_args[1]["json"]["headers"] == {"Authorization": "Bearer secret-token"}
|
|
|
|
|
|
@pytest.mark.unit
|
|
async def test_delete_webhook(webhooks_client, mocker):
|
|
"""Test deleting a webhook registration."""
|
|
mock_response = mocker.Mock()
|
|
|
|
mock_make_request = mocker.patch.object(
|
|
WebhooksClient, "_make_request", return_value=mock_response
|
|
)
|
|
|
|
await webhooks_client.delete_webhook(webhook_id=123)
|
|
|
|
mock_make_request.assert_called_once_with(
|
|
"DELETE",
|
|
"/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks/123",
|
|
headers={"OCS-APIRequest": "true", "Accept": "application/json"},
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
async def test_get_webhook(webhooks_client, mocker):
|
|
"""Test getting a specific webhook by ID."""
|
|
mock_response = mocker.Mock()
|
|
mock_response.json.return_value = {
|
|
"ocs": {
|
|
"data": {
|
|
"id": 123,
|
|
"uri": "http://example.com/webhook",
|
|
"event": "OCP\\Files\\Events\\Node\\NodeCreatedEvent",
|
|
"httpMethod": "POST",
|
|
}
|
|
}
|
|
}
|
|
|
|
mock_make_request = mocker.patch.object(
|
|
WebhooksClient, "_make_request", return_value=mock_response
|
|
)
|
|
|
|
webhook_data = await webhooks_client.get_webhook(webhook_id=123)
|
|
|
|
assert webhook_data["id"] == 123
|
|
assert webhook_data["event"] == "OCP\\Files\\Events\\Node\\NodeCreatedEvent"
|
|
|
|
mock_make_request.assert_called_once_with(
|
|
"GET",
|
|
"/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks/123",
|
|
headers={"OCS-APIRequest": "true", "Accept": "application/json"},
|
|
)
|