fix(528): pass3: PUT with RECURRENCE-ID
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
# =============================================================
|
||||
# OxiCloud – CalDAV recurring events with RECURRENCE-ID overrides
|
||||
# =============================================================
|
||||
# End-to-end regression for AtalayaLabs/OxiCloud#528.
|
||||
#
|
||||
# Pre-fix behaviour (all-day recurring event, one occurrence
|
||||
# modified in Thunderbird/Apple Calendar/DAVx⁵/Gnome Calendar):
|
||||
# * The client PUTs a VCALENDAR containing the master (with
|
||||
# RRULE) + a per-instance override (RFC 5545 §3.8.4.4,
|
||||
# `RECURRENCE-ID`). Pre-fix the substring-based parser
|
||||
# could not read any property carrying parameters
|
||||
# (`DTSTART;VALUE=DATE:...`, `RECURRENCE-ID;VALUE=DATE:...`),
|
||||
# so all-day master modifications 500'd outright.
|
||||
# * Even for timed events, the old create_event_from_ical
|
||||
# read only the first VEVENT — a second PUT of just the
|
||||
# exception would overwrite the master row entirely,
|
||||
# silently corrupting the client's view of the series.
|
||||
#
|
||||
# Post-fix (this file's invariant):
|
||||
# 1. Master PUT → 201 CREATED, one row (recurrence_id NULL).
|
||||
# 2. PUT master + exception in one body → both persist to
|
||||
# their own row keyed by (calendar_id, ical_uid,
|
||||
# recurrence_id). Response is 201 CREATED because the
|
||||
# exception was newly inserted.
|
||||
# 3. PUT ONLY the exception with modified content → 204
|
||||
# No Content (in-place replace, no new rows). CRITICALLY,
|
||||
# the MASTER row survives untouched — a GET on the .ics
|
||||
# URL still returns the master's original RRULE + summary.
|
||||
# 4. All-day master + all-day exception (the exact #528 shape)
|
||||
# completes the same round-trip.
|
||||
#
|
||||
# Storage invariant enforced by two partial unique indexes on
|
||||
# caldav.calendar_events (see migration 20260913000001):
|
||||
# * idx_calendar_events_master_unique — at most one master
|
||||
# per (calendar_id, ical_uid).
|
||||
# * idx_calendar_events_exception_unique — at most one
|
||||
# exception override per (calendar_id, ical_uid,
|
||||
# recurrence_id).
|
||||
# =============================================================
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 1 – Admin logs in.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{
|
||||
"username": "{{username}}",
|
||||
"password": "{{password}}"
|
||||
}
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
admin_token: jsonpath "$.access_token"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 2 – MKCALENDAR: fresh calendar for #528 regression.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
MKCALENDAR {{base_url}}/caldav/recurring-528/
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 201
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 3 – PROPFIND to capture the server-assigned UUID for
|
||||
# recurring-528. The (?s).* anchor greedy-matches to the LAST
|
||||
# /caldav/<uuid>/ in the body, which is our just-created
|
||||
# calendar (default-provisioned calendars come first by
|
||||
# created_at, this one is newest).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PROPFIND {{base_url}}/caldav/
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Depth: 1
|
||||
Content-Type: application/xml
|
||||
```
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:prop>
|
||||
<D:displayname/>
|
||||
<D:resourcetype/>
|
||||
</D:prop>
|
||||
</D:propfind>
|
||||
```
|
||||
|
||||
HTTP 207
|
||||
[Captures]
|
||||
calendar_id: body regex "(?s).*/caldav/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/"
|
||||
[Asserts]
|
||||
body contains "recurring-528"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 4 – PUT the recurring master (timed, daily, 10 count).
|
||||
# Expect 201 CREATED (fresh row) and a non-empty ETag.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PUT {{base_url}}/caldav/{{calendar_id}}/daily-e2e-528.ics
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: text/calendar; charset=utf-8
|
||||
```
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//OxiCloud e2e//EN
|
||||
BEGIN:VEVENT
|
||||
UID:daily-e2e-528
|
||||
DTSTAMP:20260101T100000Z
|
||||
DTSTART:20260101T090000Z
|
||||
DTEND:20260101T093000Z
|
||||
SUMMARY:Daily standup
|
||||
RRULE:FREQ=DAILY;COUNT=10
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
```
|
||||
|
||||
HTTP 201
|
||||
[Asserts]
|
||||
header "ETag" exists
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 5 – GET the master. Body contains RRULE + original
|
||||
# SUMMARY, confirming the master is stored and serves as-is.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/caldav/{{calendar_id}}/daily-e2e-528.ics
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "FREQ=DAILY;COUNT=10"
|
||||
body contains "SUMMARY:Daily standup"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 6 – The #528 heart: PUT master + per-instance override
|
||||
# in a single body. This is what Thunderbird sends when the
|
||||
# user modifies one occurrence of a recurring event.
|
||||
#
|
||||
# Expected:
|
||||
# * 201 CREATED because the exception is newly inserted.
|
||||
# (The master is replaced-in-place — any_inserted=true
|
||||
# is decided by the NEW exception row, not the master.)
|
||||
# * Both rows now exist in the DB. Verified in Step 7 via
|
||||
# the master's GET still returning the master data.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PUT {{base_url}}/caldav/{{calendar_id}}/daily-e2e-528.ics
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: text/calendar; charset=utf-8
|
||||
```
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//OxiCloud e2e//EN
|
||||
BEGIN:VEVENT
|
||||
UID:daily-e2e-528
|
||||
DTSTAMP:20260101T100000Z
|
||||
DTSTART:20260101T090000Z
|
||||
DTEND:20260101T093000Z
|
||||
SUMMARY:Daily standup
|
||||
RRULE:FREQ=DAILY;COUNT=10
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:daily-e2e-528
|
||||
DTSTAMP:20260101T100000Z
|
||||
DTSTART:20260103T110000Z
|
||||
DTEND:20260103T120000Z
|
||||
SUMMARY:Daily standup — rescheduled
|
||||
RECURRENCE-ID:20260103T090000Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
```
|
||||
|
||||
HTTP 201
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 7 – GET the master. It must STILL be the master (with
|
||||
# RRULE + original SUMMARY). Pre-fix the exception would have
|
||||
# clobbered this row and Step 7 would see the exception's
|
||||
# SUMMARY ("… rescheduled") without the RRULE.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/caldav/{{calendar_id}}/daily-e2e-528.ics
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "FREQ=DAILY;COUNT=10"
|
||||
body contains "SUMMARY:Daily standup"
|
||||
body not contains "SUMMARY:Daily standup — rescheduled"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 8 – PUT only the exception with a modified SUMMARY.
|
||||
# Because the exception row already exists (from Step 6),
|
||||
# no new row is inserted → 204 No Content. The MASTER is
|
||||
# untouched (verified in Step 9).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PUT {{base_url}}/caldav/{{calendar_id}}/daily-e2e-528.ics
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: text/calendar; charset=utf-8
|
||||
```
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//OxiCloud e2e//EN
|
||||
BEGIN:VEVENT
|
||||
UID:daily-e2e-528
|
||||
DTSTAMP:20260101T110000Z
|
||||
DTSTART:20260103T120000Z
|
||||
DTEND:20260103T130000Z
|
||||
SUMMARY:Daily standup — rescheduled AGAIN
|
||||
RECURRENCE-ID:20260103T090000Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
```
|
||||
|
||||
HTTP 204
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 9 – Master survives the exception update. Pre-fix this
|
||||
# would fail: the old delete-by-UID-then-insert path would
|
||||
# have removed the master when the exception-only PUT landed.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/caldav/{{calendar_id}}/daily-e2e-528.ics
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "FREQ=DAILY;COUNT=10"
|
||||
body contains "SUMMARY:Daily standup"
|
||||
body not contains "SUMMARY:Daily standup — rescheduled"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 10 – The all-day flavour: master with DTSTART;VALUE=DATE
|
||||
# + exception with RECURRENCE-ID;VALUE=DATE. Pre-parser-rewrite
|
||||
# this 500'd because the param-carrying property lines were
|
||||
# invisible to the substring scanner (root cause of #528).
|
||||
#
|
||||
# Uses a distinct UID so it doesn't collide with Step 4-8 rows
|
||||
# under the master partial unique index.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PUT {{base_url}}/caldav/{{calendar_id}}/weekly-allday-528.ics
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: text/calendar; charset=utf-8
|
||||
```
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//OxiCloud e2e//EN
|
||||
BEGIN:VEVENT
|
||||
UID:weekly-allday-528
|
||||
DTSTAMP:20260101T100000Z
|
||||
DTSTART;VALUE=DATE:20260105
|
||||
DTEND;VALUE=DATE:20260106
|
||||
SUMMARY:Weekly review
|
||||
RRULE:FREQ=WEEKLY;COUNT=4
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:weekly-allday-528
|
||||
DTSTAMP:20260101T100000Z
|
||||
DTSTART;VALUE=DATE:20260113
|
||||
DTEND;VALUE=DATE:20260114
|
||||
SUMMARY:Weekly review — moved
|
||||
RECURRENCE-ID;VALUE=DATE:20260112
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
```
|
||||
|
||||
HTTP 201
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 11 – Cleanup: delete the entire calendar (cascades to
|
||||
# all events + exception rows in a single storage call). Keeps
|
||||
# the shared Hurl DB uncluttered for downstream test files
|
||||
# (per feedback_hurl_teardown_shared_db).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
DELETE {{base_url}}/caldav/{{calendar_id}}/
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 204
|
||||
@@ -168,6 +168,7 @@ hurl --variables-file "$API_DIR/test.env" --file-root "$REPO_ROOT/tests" --test
|
||||
"$API_DIR/dav_error_mapping.hurl" \
|
||||
"$API_DIR/contacts.hurl" \
|
||||
"$API_DIR/calendar.hurl" \
|
||||
"$API_DIR/caldav_recurring.hurl" \
|
||||
"$API_DIR/playlists.hurl" \
|
||||
"$API_DIR/public_shares.hurl" \
|
||||
"$API_DIR/permissions.hurl" \
|
||||
|
||||
Reference in New Issue
Block a user