Files
Oxicloud/tests/api/caldav_recurring.hurl
T

295 lines
12 KiB
Plaintext
Raw Normal View History

2026-07-14 17:46:24 +02:00
# =============================================================
# 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
# ─────────────────────────────────────────────────────────────
2026-07-14 23:58:03 +02:00
# Step 7 – GET the URL — must return the FULL calendar-object-
# resource: master VEVENT (with RRULE + original SUMMARY) AND
# the exception VEVENT (with RECURRENCE-ID + rescheduled
# SUMMARY) concatenated in ONE VCALENDAR body. This is the
# phase-4 read-side contract per RFC 4791 §4.1 + RFC 5545
# §3.6.1 — one URL per UID, one VCALENDAR containing every
# component.
#
# Pre-phase-4 this GET returned ONLY the master and clients
# never saw the exception, so their next-PUT dropped it.
2026-07-14 17:46:24 +02:00
# ─────────────────────────────────────────────────────────────
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"
2026-07-14 23:58:03 +02:00
body contains "SUMMARY:Daily standup — rescheduled"
body contains "RECURRENCE-ID:20260103T090000Z"
2026-07-14 17:46:24 +02:00
# ─────────────────────────────────────────────────────────────
# 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
# ─────────────────────────────────────────────────────────────
2026-07-14 23:58:03 +02:00
# Step 9 – After the exception-only PUT: bundled GET returns
# the master (unchanged, still carries RRULE + original
# SUMMARY) AND the newly-updated exception (SUMMARY now
# "rescheduled AGAIN" from Step 8).
#
# Pre-phase-3 the exception-only PUT wiped the master row.
# Pre-phase-4 the master survived but the exception was
# invisible in the GET body.
# Post-phase-4: both survive, both visible.
2026-07-14 17:46:24 +02:00
# ─────────────────────────────────────────────────────────────
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"
2026-07-14 23:58:03 +02:00
body contains "SUMMARY:Daily standup — rescheduled AGAIN"
body contains "RECURRENCE-ID:20260103T090000Z"
2026-07-14 17:46:24 +02:00
# ─────────────────────────────────────────────────────────────
# 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