b58e7238ae
Manual testing of Nextcloud webhook_listeners app to validate webhook payloads against ADR-010 expected schemas and document implementation requirements for webhook-based vector synchronization. ## Changes - Add test webhook endpoint at /webhooks/nextcloud in app.py - Captures and logs webhook payloads for analysis - Returns 200 OK immediately for webhook delivery confirmation - Create webhook-testing-findings.md with comprehensive test results - Captured payloads for 5/6 webhook event types - Critical findings: missing node.id in deletions, type mismatches - Implementation recommendations with code examples - Update ADR-010 with Appendix A: Manual Webhook Testing Results - Document actual vs expected webhook behavior - Update event mapping table with tested webhook status - Add 6 specific implementation recommendations - Include testing implications for future development ## Testing Results ✅ NodeCreatedEvent - fires correctly, includes node.id (integer) ✅ NodeWrittenEvent - fires correctly, includes node.id (integer) ✅ NodeDeletedEvent - fires but missing node.id field (path only) ✅ CalendarObjectCreatedEvent - fires correctly with full iCal ✅ CalendarObjectUpdatedEvent - fires correctly with full iCal ❌ CalendarObjectDeletedEvent - does not fire (potential NC bug) ## Key Findings 1. NodeDeletedEvent missing node.id field - requires path-based fallback 2. node.id returns integer not string - needs casting for consistency 3. Multiple webhooks fire per operation - needs deduplication logic 4. Calendar deletion webhooks don't fire - reported as issue #53497 5. Calendar webhooks include full iCal content - enables rich parsing ## GitHub Issues - Created issue #56371: NodeDeletedEvent missing node.id field - Commented on issue #53497: CalendarObjectDeletedEvent not firing Closes #283 --- _This commit was generated with the help of AI, and reviewed by a Human_
533 lines
17 KiB
Markdown
533 lines
17 KiB
Markdown
# Nextcloud Webhook Testing Findings
|
|
|
|
**Date:** 2025-11-11
|
|
**Purpose:** Manual validation of Nextcloud webhook schemas and behavior for vector sync integration (ADR-010)
|
|
|
|
## Executive Summary
|
|
|
|
Successfully tested and validated Nextcloud webhook payloads for file/note events and calendar events. **5 out of 6** webhook types were captured and validated against expected schemas from ADR-010 and Nextcloud documentation. One calendar deletion webhook did not fire during testing (potential Nextcloud issue or configuration).
|
|
|
|
## Test Environment
|
|
|
|
- **Nextcloud Version:** 30+ (Docker compose setup)
|
|
- **Webhook App:** `webhook_listeners` (bundled, enabled)
|
|
- **MCP Server:** Test endpoint at `http://mcp:8000/webhooks/nextcloud`
|
|
- **Background Worker:** Running with 60s timeout
|
|
- **Authentication:** None (test environment)
|
|
|
|
## Webhooks Registered
|
|
|
|
| ID | Event Class | Status |
|
|
|----|------------|--------|
|
|
| 1 | `OCP\Files\Events\Node\NodeCreatedEvent` | ✓ Tested |
|
|
| 2 | `OCP\Files\Events\Node\NodeWrittenEvent` | ✓ Tested |
|
|
| 3 | `OCP\Files\Events\Node\NodeDeletedEvent` | ✓ Tested |
|
|
| 4 | `OCP\Calendar\Events\CalendarObjectCreatedEvent` | ✓ Tested |
|
|
| 5 | `OCP\Calendar\Events\CalendarObjectUpdatedEvent` | ✓ Tested |
|
|
| 6 | `OCP\Calendar\Events\CalendarObjectDeletedEvent` | ✗ Not received |
|
|
|
|
## Captured Webhook Payloads
|
|
|
|
### 1. NodeCreatedEvent (File/Note Creation)
|
|
|
|
**Test Action:** Created note via Notes API
|
|
**Trigger Time:** 2025-11-11 08:37:25
|
|
**Webhooks Fired:** 3 events (folder creation + file creation + file written)
|
|
|
|
**Payload:**
|
|
```json
|
|
{
|
|
"user": {
|
|
"uid": "admin",
|
|
"displayName": "admin"
|
|
},
|
|
"time": 1762850245,
|
|
"event": {
|
|
"class": "OCP\\Files\\Events\\Node\\NodeCreatedEvent",
|
|
"node": {
|
|
"id": 437,
|
|
"path": "/admin/files/Notes/Webhooks/Webhook Test Note.md"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Validation:**
|
|
- ✅ Schema matches ADR-010 specification
|
|
- ✅ Contains `user` object with `uid` and `displayName`
|
|
- ✅ Contains `time` (Unix timestamp)
|
|
- ✅ Contains `event.class` (fully qualified event name)
|
|
- ✅ Contains `event.node.id` (file ID)
|
|
- ✅ Contains `event.node.path` (absolute path)
|
|
|
|
**Observations:**
|
|
- Creating a note via Notes API triggers 3 webhook events:
|
|
1. `NodeCreatedEvent` for the parent folder (if new)
|
|
2. `NodeWrittenEvent` for the parent folder
|
|
3. `NodeCreatedEvent` for the actual file
|
|
4. `NodeWrittenEvent` for the file (sometimes fired 2x)
|
|
|
|
### 2. NodeWrittenEvent (File/Note Update)
|
|
|
|
**Test Action:** Updated note content via Notes API
|
|
**Trigger Time:** 2025-11-11 08:49:20
|
|
|
|
**Payload:**
|
|
```json
|
|
{
|
|
"user": {
|
|
"uid": "admin",
|
|
"displayName": "admin"
|
|
},
|
|
"time": 1762850960,
|
|
"event": {
|
|
"class": "OCP\\Files\\Events\\Node\\NodeWrittenEvent",
|
|
"node": {
|
|
"id": 437,
|
|
"path": "/admin/files/Notes/Webhooks/Webhook Test Note.md"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Validation:**
|
|
- ✅ Schema identical to `NodeCreatedEvent` except for `event.class`
|
|
- ✅ Same file ID (437) as creation event
|
|
- ✅ Updated timestamp reflects actual modification time
|
|
|
|
**Observations:**
|
|
- File updates trigger a single `NodeWrittenEvent`
|
|
- No duplicate events fired for update operations
|
|
|
|
### 3. NodeDeletedEvent (File/Note Deletion)
|
|
|
|
**Test Action:** Deleted note via Notes API
|
|
**Trigger Time:** 2025-11-11 08:51:34
|
|
**Webhooks Fired:** 2 events (file + folder deletion)
|
|
|
|
**Payload:**
|
|
```json
|
|
{
|
|
"user": {
|
|
"uid": "admin",
|
|
"displayName": "admin"
|
|
},
|
|
"time": 1762851093,
|
|
"event": {
|
|
"class": "OCP\\Files\\Events\\Node\\NodeDeletedEvent",
|
|
"node": {
|
|
"path": "/admin/files/Notes/Webhooks/Webhook Test Note.md"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Validation:**
|
|
- ✅ Schema matches ADR-010 specification
|
|
- ⚠️ **IMPORTANT:** No `node.id` field in deletion events (only `path`)
|
|
- ✅ Folder deletion triggered after file deletion (empty folder cleanup)
|
|
|
|
**Observations:**
|
|
- **Critical Difference:** Deletion events do NOT include `node.id`, only `node.path`
|
|
- This differs from Create/Write events which include both `id` and `path`
|
|
- ADR-010 implementation must handle missing `id` field for deletions
|
|
- Deleting a file also triggers deletion of empty parent folders
|
|
|
|
### 4. CalendarObjectCreatedEvent (Calendar Event Creation)
|
|
|
|
**Test Action:** Created calendar event via CalDAV PUT
|
|
**Trigger Time:** 2025-11-11 08:52:50
|
|
|
|
**Payload (partial - calendarData omitted for brevity):**
|
|
```json
|
|
{
|
|
"user": {
|
|
"uid": "admin",
|
|
"displayName": "admin"
|
|
},
|
|
"time": 1762851169,
|
|
"event": {
|
|
"calendarId": 1,
|
|
"class": "OCP\\Calendar\\Events\\CalendarObjectCreatedEvent",
|
|
"calendarData": {
|
|
"id": 1,
|
|
"uri": "personal",
|
|
"{http://calendarserver.org/ns/}getctag": "...",
|
|
"{http://sabredav.org/ns}sync-token": 21,
|
|
"{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set": [],
|
|
"{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp": [],
|
|
"{urn:ietf:params:xml:ns:caldav}calendar-timezone": null
|
|
},
|
|
"objectData": {
|
|
"id": 3,
|
|
"uri": "webhook-test-event-001.ics",
|
|
"lastmodified": 1762851169,
|
|
"etag": "\"2b937b7d77dc83c77329dfdb210ba9d0\"",
|
|
"calendarid": 1,
|
|
"size": 297,
|
|
"component": "vevent",
|
|
"classification": 0,
|
|
"uid": "webhook-test-event-001@nextcloud",
|
|
"calendardata": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n...",
|
|
"{http://nextcloud.com/ns}deleted-at": null
|
|
},
|
|
"shares": []
|
|
}
|
|
}
|
|
```
|
|
|
|
**Validation:**
|
|
- ✅ Schema matches Nextcloud documentation
|
|
- ✅ Contains complete calendar metadata (`calendarData`)
|
|
- ✅ Contains complete event data (`objectData`)
|
|
- ✅ Includes full iCal data in `objectData.calendardata`
|
|
- ✅ Includes `objectData.id` for database lookups
|
|
- ⚠️ **Complex:** Much more metadata than file events
|
|
|
|
**Observations:**
|
|
- Calendar webhooks include significantly more data than file webhooks
|
|
- Full iCal content is embedded in `objectData.calendardata`
|
|
- Event ID is in `objectData.id` (NOT `event.id`)
|
|
- `calendarData` contains calendar-level metadata
|
|
- `shares` array contains sharing information (empty in this test)
|
|
|
|
### 5. CalendarObjectUpdatedEvent (Calendar Event Update)
|
|
|
|
**Test Action:** Updated calendar event via CalDAV PUT
|
|
**Trigger Time:** 2025-11-11 08:53:28
|
|
|
|
**Payload (partial):**
|
|
```json
|
|
{
|
|
"user": {
|
|
"uid": "admin",
|
|
"displayName": "admin"
|
|
},
|
|
"time": 1762851207,
|
|
"event": {
|
|
"calendarId": 1,
|
|
"class": "OCP\\Calendar\\Events\\CalendarObjectUpdatedEvent",
|
|
"calendarData": { /* same structure as creation */ },
|
|
"objectData": {
|
|
"id": 3,
|
|
"uri": "webhook-test-event-001.ics",
|
|
"lastmodified": 1762851207,
|
|
"etag": "\"2695a18013e0991e4212b07b61d5e1e2\"",
|
|
"calendarid": 1,
|
|
"size": 315,
|
|
"component": "vevent",
|
|
"classification": 0,
|
|
"uid": "webhook-test-event-001@nextcloud",
|
|
"calendardata": "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n...",
|
|
"{http://nextcloud.com/ns}deleted-at": null
|
|
},
|
|
"shares": []
|
|
}
|
|
}
|
|
```
|
|
|
|
**Validation:**
|
|
- ✅ Schema identical to `CalendarObjectCreatedEvent` except `event.class`
|
|
- ✅ Same event ID (3) as creation
|
|
- ✅ Updated `lastmodified` timestamp
|
|
- ✅ Different `etag` (changed from creation)
|
|
- ✅ Larger `size` (315 vs 297 bytes)
|
|
|
|
**Observations:**
|
|
- Update events contain full new state (not delta)
|
|
- ETag changes on updates (useful for conflict detection)
|
|
- Size field reflects actual iCal size
|
|
|
|
### 6. CalendarObjectDeletedEvent (Calendar Event Deletion)
|
|
|
|
**Test Action:** Deleted calendar event via CalDAV DELETE
|
|
**Trigger Time:** 2025-11-11 08:54:47
|
|
**Status:** ❌ **WEBHOOK DID NOT FIRE**
|
|
|
|
**Expected Payload (from Nextcloud docs):**
|
|
```json
|
|
{
|
|
"user": {
|
|
"uid": "admin",
|
|
"displayName": "admin"
|
|
},
|
|
"time": <timestamp>,
|
|
"event": {
|
|
"calendarId": 1,
|
|
"class": "OCP\\Calendar\\Events\\CalendarObjectDeletedEvent",
|
|
"calendarData": { /* calendar metadata */ },
|
|
"objectData": {
|
|
"id": 3,
|
|
"uri": "webhook-test-event-001.ics",
|
|
/* ... other fields ... */
|
|
},
|
|
"shares": []
|
|
}
|
|
}
|
|
```
|
|
|
|
**Issue:**
|
|
- Calendar event was successfully deleted (verified via CalDAV PROPFIND)
|
|
- Webhook registration confirmed (ID #6 in `webhook_listeners:list`)
|
|
- Background worker running and processing other events
|
|
- **No webhook notification received after 2+ minutes**
|
|
|
|
**Possible Causes:**
|
|
1. Known Nextcloud bug with calendar deletion webhooks
|
|
2. CalDAV DELETE may not trigger event system properly
|
|
3. Deletion event may require trash bin enabled
|
|
4. Background job may have silently failed
|
|
|
|
**Recommended Actions:**
|
|
- File Nextcloud issue report
|
|
- Test with trash bin enabled (`CalendarObjectMovedToTrashEvent`)
|
|
- Check Nextcloud error logs for webhook failures
|
|
- Verify with Nextcloud 31+ if issue persists
|
|
|
|
## Schema Comparison: Expected vs Actual
|
|
|
|
### File Events
|
|
|
|
| Field | Expected (ADR-010) | Actual | Match |
|
|
|-------|-------------------|--------|-------|
|
|
| `user.uid` | string | string | ✅ |
|
|
| `user.displayName` | string | string | ✅ |
|
|
| `time` | int | int | ✅ |
|
|
| `event.class` | string | string | ✅ |
|
|
| `event.node.id` | string | int | ⚠️ Type mismatch |
|
|
| `event.node.path` | string | string | ✅ |
|
|
|
|
**Type Discrepancy:** `node.id` is documented as `string` but returns as `int` (437 instead of "437")
|
|
|
|
### Calendar Events
|
|
|
|
| Field | Expected (Nextcloud docs) | Actual | Match |
|
|
|-------|-------------------------|--------|-------|
|
|
| `user.uid` | string | string | ✅ |
|
|
| `user.displayName` | string | string | ✅ |
|
|
| `time` | int | int | ✅ |
|
|
| `event.class` | string | string | ✅ |
|
|
| `event.calendarId` | int | int | ✅ |
|
|
| `event.calendarData.*` | object | object | ✅ |
|
|
| `event.objectData.id` | int | int | ✅ |
|
|
| `event.objectData.uri` | string | string | ✅ |
|
|
| `event.objectData.calendardata` | string | string | ✅ |
|
|
| `event.objectData.lastmodified` | int | int | ✅ |
|
|
| `event.objectData.etag` | string | string | ✅ |
|
|
| `event.objectData.component` | string\|null | string | ✅ |
|
|
| `event.shares` | array | array | ✅ |
|
|
|
|
All calendar event fields match expected schemas.
|
|
|
|
## Key Findings for ADR-010 Implementation
|
|
|
|
### 1. Deletion Events Have Different Schema
|
|
- **File Deletions:** No `node.id` field, only `node.path`
|
|
- **Calendar Deletions:** Not tested (webhook didn't fire)
|
|
- **Impact:** Webhook handler must check for `node.id` existence before using it
|
|
|
|
### 2. Multiple Webhooks Per Operation
|
|
- Creating a note triggers 3-5 webhook events
|
|
- Deleting a note triggers 2 events (file + folder)
|
|
- **Impact:** Deduplication logic needed in webhook handler
|
|
|
|
### 3. Event-Specific ID Fields
|
|
- **File events:** `event.node.id`
|
|
- **Calendar events:** `event.objectData.id`
|
|
- **Impact:** Event parser must handle different ID field locations
|
|
|
|
### 4. Full State vs Delta
|
|
- All webhooks contain complete current state (not delta)
|
|
- **Impact:** No need for "previous state" tracking in webhook handler
|
|
|
|
### 5. Calendar Data Richness
|
|
- Calendar webhooks include full iCal content
|
|
- **Impact:** Can extract all event metadata without additional API calls
|
|
|
|
## Recommendations for ADR-010 Implementation
|
|
|
|
### 1. Webhook Event Parser (`webhook_parser.py`)
|
|
|
|
```python
|
|
def extract_document_task(event_class: str, payload: dict) -> DocumentTask | None:
|
|
"""Extract DocumentTask from webhook event payload."""
|
|
user_id = payload["user"]["uid"]
|
|
event_data = payload["event"]
|
|
|
|
# File/Note events
|
|
if "NodeCreatedEvent" in event_class or "NodeWrittenEvent" in event_class:
|
|
path = event_data["node"]["path"]
|
|
|
|
# Only process markdown files for notes
|
|
if not path.endswith(".md"):
|
|
return None
|
|
|
|
# IMPORTANT: Check if 'id' exists (missing in deletion events)
|
|
doc_id = str(event_data["node"].get("id", ""))
|
|
if not doc_id:
|
|
# For missing ID, use path-based identifier
|
|
doc_id = f"path:{path}"
|
|
|
|
return DocumentTask(
|
|
user_id=user_id,
|
|
doc_id=doc_id,
|
|
doc_type="note",
|
|
operation="index",
|
|
modified_at=payload["time"],
|
|
)
|
|
|
|
# File deletion events
|
|
elif "NodeDeletedEvent" in event_class:
|
|
path = event_data["node"]["path"]
|
|
|
|
if not path.endswith(".md"):
|
|
return None
|
|
|
|
# Deletion events DON'T have node.id - use path
|
|
return DocumentTask(
|
|
user_id=user_id,
|
|
doc_id=f"path:{path}", # Path-based since ID unavailable
|
|
doc_type="note",
|
|
operation="delete",
|
|
modified_at=payload["time"],
|
|
)
|
|
|
|
# Calendar creation/update events
|
|
elif "CalendarObjectCreatedEvent" in event_class or \
|
|
"CalendarObjectUpdatedEvent" in event_class:
|
|
return DocumentTask(
|
|
user_id=user_id,
|
|
doc_id=str(event_data["objectData"]["id"]),
|
|
doc_type="calendar_event",
|
|
operation="index",
|
|
modified_at=event_data["objectData"]["lastmodified"],
|
|
)
|
|
|
|
# Calendar deletion events
|
|
elif "CalendarObjectDeletedEvent" in event_class:
|
|
return DocumentTask(
|
|
user_id=user_id,
|
|
doc_id=str(event_data["objectData"]["id"]),
|
|
doc_type="calendar_event",
|
|
operation="delete",
|
|
modified_at=payload["time"],
|
|
)
|
|
|
|
return None # Unsupported event type
|
|
```
|
|
|
|
### 2. Deduplication Strategy
|
|
|
|
**Problem:** Creating a note triggers 3-5 webhooks
|
|
**Solution:** Idempotent processing + task deduplication
|
|
|
|
```python
|
|
# In webhook handler
|
|
async def handle_nextcloud_webhook(request: Request) -> JSONResponse:
|
|
payload = await request.json()
|
|
|
|
task = extract_document_task(
|
|
payload["event"]["class"],
|
|
payload
|
|
)
|
|
|
|
if task:
|
|
# Idempotent: Queue will only process latest version
|
|
await document_queue.send(task)
|
|
|
|
return JSONResponse({"status": "received"}, status_code=200)
|
|
```
|
|
|
|
### 3. Path-Based Fallback for Deletions
|
|
|
|
Since deletion events lack `node.id`, use path-based identification:
|
|
|
|
```python
|
|
# In Qdrant delete logic
|
|
async def delete_document(user_id: str, doc_id: str, doc_type: str):
|
|
if doc_id.startswith("path:"):
|
|
# Path-based deletion
|
|
path = doc_id.removeprefix("path:")
|
|
# Search Qdrant for document with matching path in metadata
|
|
points = await qdrant.scroll(
|
|
collection_name=collection,
|
|
scroll_filter=Filter(must=[
|
|
FieldCondition(
|
|
key="user_id",
|
|
match=MatchValue(value=user_id),
|
|
),
|
|
FieldCondition(
|
|
key="metadata.path",
|
|
match=MatchValue(value=path),
|
|
),
|
|
]),
|
|
)
|
|
# Delete found points
|
|
else:
|
|
# ID-based deletion (normal case)
|
|
...
|
|
```
|
|
|
|
### 4. Webhook Registration Filters
|
|
|
|
To reduce webhook volume, add filters:
|
|
|
|
```json
|
|
{
|
|
"httpMethod": "POST",
|
|
"uri": "http://mcp:8000/webhooks/nextcloud",
|
|
"event": "OCP\\Files\\Events\\Node\\NodeCreatedEvent",
|
|
"eventFilter": {
|
|
"event.node.path": "/^.*\\.md$/"
|
|
}
|
|
}
|
|
```
|
|
|
|
This filters to only `.md` files at the webhook registration level (not handler level).
|
|
|
|
### 5. Monitoring and Metrics
|
|
|
|
Add webhook-specific metrics:
|
|
|
|
```python
|
|
webhook_notifications_received_total{event_type="note_created"} 42
|
|
webhook_processing_duration_seconds{event_type="note_created"} 0.023
|
|
webhook_errors_total{error_type="parse_error"} 2
|
|
webhook_duplicates_filtered_total{doc_type="note"} 15
|
|
```
|
|
|
|
## Testing Checklist for Implementation
|
|
|
|
- [x] File creation webhook triggers document indexing
|
|
- [x] File update webhook triggers reindexing
|
|
- [x] File deletion webhook triggers document removal
|
|
- [ ] File deletion without ID successfully removes document (path-based)
|
|
- [x] Calendar creation webhook triggers event indexing
|
|
- [x] Calendar update webhook triggers event reindexing
|
|
- [ ] Calendar deletion webhook triggers event removal (NOT TESTED - webhook didn't fire)
|
|
- [ ] Duplicate webhooks are deduplicated
|
|
- [ ] Non-markdown file webhooks are ignored
|
|
- [ ] Malformed webhook payloads return 400 error
|
|
- [ ] Webhook authentication validates shared secret
|
|
- [ ] Webhook processing completes within 50ms
|
|
|
|
## Appendix: Raw Webhook Logs
|
|
|
|
Complete webhook logs with full payloads are available in MCP container logs:
|
|
|
|
```bash
|
|
docker compose logs mcp | grep -A 30 "🔔 Webhook received"
|
|
```
|
|
|
|
## Conclusion
|
|
|
|
Nextcloud webhooks work as documented with minor exceptions:
|
|
|
|
1. ✅ **File/Note Events:** Fully functional and match expected schemas
|
|
2. ✅ **Calendar Creation/Update:** Fully functional with rich metadata
|
|
3. ❌ **Calendar Deletion:** Webhook did not fire (requires investigation)
|
|
4. ⚠️ **Schema Discrepancy:** `node.id` is integer (not string as documented)
|
|
5. ⚠️ **Deletion Schema:** Missing `node.id` field (only `path` provided)
|
|
|
|
**Overall Status:** Ready for ADR-010 implementation with noted caveats. Calendar deletion webhook issue should be reported to Nextcloud and may require alternative approach (polling or trash bin events).
|