Files
Oxicloud/tests/api/caldav_recurring.hurl
T
2026-07-14 20:42:18 +02:00

281 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================
# 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