fix(caldav): fix generation of events
keep information of: ATTENDEE, ORGANIZER, CATEGORIES, STATUS, TRANSP, VALARM, X-*
this fix answer in all calldav GET
This commit is contained in:
@@ -173,10 +173,16 @@ 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.
|
||||
# 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.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/caldav/{{calendar_id}}/daily-e2e-528.ics
|
||||
Authorization: Bearer {{admin_token}}
|
||||
@@ -185,7 +191,8 @@ HTTP 200
|
||||
[Asserts]
|
||||
body contains "FREQ=DAILY;COUNT=10"
|
||||
body contains "SUMMARY:Daily standup"
|
||||
body not contains "SUMMARY:Daily standup — rescheduled"
|
||||
body contains "SUMMARY:Daily standup — rescheduled"
|
||||
body contains "RECURRENCE-ID:20260103T090000Z"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@@ -216,9 +223,15 @@ 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.
|
||||
# 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.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/caldav/{{calendar_id}}/daily-e2e-528.ics
|
||||
Authorization: Bearer {{admin_token}}
|
||||
@@ -227,7 +240,8 @@ HTTP 200
|
||||
[Asserts]
|
||||
body contains "FREQ=DAILY;COUNT=10"
|
||||
body contains "SUMMARY:Daily standup"
|
||||
body not contains "SUMMARY:Daily standup — rescheduled"
|
||||
body contains "SUMMARY:Daily standup — rescheduled AGAIN"
|
||||
body contains "RECURRENCE-ID:20260103T090000Z"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -12,16 +12,13 @@ DTSTART / DTEND / DESCRIPTION / LOCATION / RRULE / DTSTAMP /
|
||||
CREATED / LAST-MODIFIED). Anything not in that list is silently
|
||||
dropped even though the original `ical_data` is stored intact.
|
||||
|
||||
Tests split into two groups:
|
||||
|
||||
* **Sanity** — properties the server emits on GET; they must
|
||||
round-trip. Regressions here would be genuine server bugs.
|
||||
|
||||
* **xfail (documented gaps)** — properties the server currently
|
||||
drops. `@pytest.mark.xfail(strict=False)` lets the suite stay
|
||||
green while making the gap visible in the pytest summary. If
|
||||
a future server fix makes one of these survive, pytest
|
||||
reports it as `XPASS` — an alert to remove the marker.
|
||||
Every test is a strict round-trip pin: PUT a vCalendar body
|
||||
carrying the property, GET the URL, assert the property is
|
||||
present in the response. Post-phase-4 the emitter serves each
|
||||
row's stored `ical_data` verbatim (folded per UID), so a
|
||||
regression on any property here means either the storage
|
||||
layer stopped preserving ical_data OR the emitter reverted
|
||||
to DTO-field regeneration.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -30,7 +27,6 @@ import textwrap
|
||||
import uuid
|
||||
|
||||
import caldav
|
||||
import pytest
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@@ -151,24 +147,16 @@ def test_uid_and_dtstamp_are_preserved(fresh_calendar: caldav.Calendar) -> None:
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Documented gaps — properties the server currently drops on
|
||||
# GET. `xfail(strict=False)` means "expected to fail; don't fail
|
||||
# the suite, but flag XPASS if it starts passing". When the
|
||||
# read-side fix lands, remove the marker.
|
||||
# Extended round-trips — properties beyond the DTO-structured
|
||||
# columns. Post-phase-4 the emitter serves each row's stored
|
||||
# `ical_data` verbatim (folded per UID), so ATTENDEE, ORGANIZER,
|
||||
# CATEGORIES, STATUS+TRANSP, VALARM (nested), custom X-* all
|
||||
# survive PUT → GET. A regression on any of these means either
|
||||
# storage stopped preserving ical_data OR the emitter reverted
|
||||
# to DTO regeneration.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
_EMITTER_GAP_REASON = (
|
||||
"GET regenerates the body from DTO fields via write_vevent "
|
||||
"(caldav_handler.rs:~770) which only emits UID / SUMMARY / "
|
||||
"DTSTART / DTEND / DESCRIPTION / LOCATION / RRULE / DTSTAMP / "
|
||||
"CREATED / LAST-MODIFIED. Every other iCal property is stored "
|
||||
"in ical_data on the row but silently dropped on read. "
|
||||
"Fix path: either serve ical_data verbatim on GET, or extend "
|
||||
"the DTO to carry the full property set."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason=_EMITTER_GAP_REASON, strict=False)
|
||||
def test_attendee_survives_round_trip(fresh_calendar: caldav.Calendar) -> None:
|
||||
uid = f"cov-attendee-{uuid.uuid4().hex[:8]}"
|
||||
body = _minimal_event(
|
||||
@@ -185,7 +173,6 @@ def test_attendee_survives_round_trip(fresh_calendar: caldav.Calendar) -> None:
|
||||
assert "alice@example.com" in fetched
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason=_EMITTER_GAP_REASON, strict=False)
|
||||
def test_organizer_survives_round_trip(fresh_calendar: caldav.Calendar) -> None:
|
||||
uid = f"cov-organizer-{uuid.uuid4().hex[:8]}"
|
||||
body = _minimal_event(
|
||||
@@ -199,7 +186,6 @@ def test_organizer_survives_round_trip(fresh_calendar: caldav.Calendar) -> None:
|
||||
assert "bob@example.com" in fetched
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason=_EMITTER_GAP_REASON, strict=False)
|
||||
def test_categories_survive_round_trip(fresh_calendar: caldav.Calendar) -> None:
|
||||
uid = f"cov-cats-{uuid.uuid4().hex[:8]}"
|
||||
body = _minimal_event(
|
||||
@@ -213,7 +199,6 @@ def test_categories_survive_round_trip(fresh_calendar: caldav.Calendar) -> None:
|
||||
assert "ENGINEERING" in fetched
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason=_EMITTER_GAP_REASON, strict=False)
|
||||
def test_status_and_transp_survive_round_trip(
|
||||
fresh_calendar: caldav.Calendar,
|
||||
) -> None:
|
||||
@@ -233,7 +218,6 @@ def test_status_and_transp_survive_round_trip(
|
||||
assert "TRANSP:TRANSPARENT" in fetched
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason=_EMITTER_GAP_REASON, strict=False)
|
||||
def test_valarm_survives_round_trip(fresh_calendar: caldav.Calendar) -> None:
|
||||
"""VALARM is a nested sub-component of VEVENT (RFC 5545 §3.6.6)
|
||||
and drives every "remind me 15 min before" popup. It lives
|
||||
@@ -268,7 +252,6 @@ def test_valarm_survives_round_trip(fresh_calendar: caldav.Calendar) -> None:
|
||||
assert "TRIGGER:-PT15M" in fetched
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason=_EMITTER_GAP_REASON, strict=False)
|
||||
def test_custom_x_property_survives_round_trip(
|
||||
fresh_calendar: caldav.Calendar,
|
||||
) -> None:
|
||||
|
||||
@@ -182,19 +182,25 @@ def test_recurring_master_plus_exception_preserves_master(
|
||||
body = _get_master_ical(fresh_calendar, uid)
|
||||
assert "RRULE:FREQ=DAILY;COUNT=10" in body, (
|
||||
"Master row lost its RRULE — the exception overwrote the master. "
|
||||
"This is the exact regression from #528.\nMaster body: " + body
|
||||
"This is the exact regression from #528.\nBundle body: " + body
|
||||
)
|
||||
assert "SUMMARY:Daily standup" in body
|
||||
|
||||
# NOTE: not asserting the exception row is client-visible here.
|
||||
# RFC 4791 §4.1 + RFC 5545 §3.8.4.4 model a recurring event with
|
||||
# per-instance overrides as ONE calendar-object-resource whose
|
||||
# VCALENDAR contains the master VEVENT + all exception VEVENTs.
|
||||
# OxiCloud currently persists them as separate rows but the
|
||||
# GET/PROPFIND emitter returns only the master (see phase-4
|
||||
# follow-up on branch feat/caldav-read-side). Once phase 4
|
||||
# lands, add: assert "RECURRENCE-ID" in body and
|
||||
# assert "rescheduled" in body.
|
||||
# Phase-4 read-side unification: the GET response is the
|
||||
# WHOLE calendar-object-resource — master + all exception
|
||||
# VEVENTs concatenated in one VCALENDAR per RFC 4791 §4.1 +
|
||||
# RFC 5545 §3.6.1. The exception's SUMMARY and its
|
||||
# RECURRENCE-ID must therefore appear alongside the master's
|
||||
# RRULE. Pre-phase-4 the emitter served only the master row
|
||||
# and clients silently dropped the exception on next-PUT.
|
||||
assert "SUMMARY:Daily standup — rescheduled" in body, (
|
||||
"Exception VEVENT missing from bundled GET body — phase-4 "
|
||||
"read-side regression.\nBundle body: " + body
|
||||
)
|
||||
assert "RECURRENCE-ID" in body, (
|
||||
"Exception RECURRENCE-ID missing from bundled GET body — "
|
||||
"clients need it to correlate the override with the master.\n"
|
||||
"Bundle body: " + body
|
||||
)
|
||||
|
||||
|
||||
def test_exception_only_put_does_not_wipe_master(
|
||||
@@ -255,24 +261,29 @@ def test_exception_only_put_does_not_wipe_master(
|
||||
),
|
||||
)
|
||||
|
||||
# Master URL GET must still return the master. Pre-fix the
|
||||
# exception-only PUT would have replaced the master (keyed by
|
||||
# UID with no recurrence_id filter) — this is the data-loss
|
||||
# half of #528.
|
||||
# Bundled GET returns the WHOLE calendar-object-resource:
|
||||
# master row (unchanged since Step 1 seed) + the updated
|
||||
# exception row (SUMMARY "rescheduled AGAIN" from the
|
||||
# exception-only PUT above).
|
||||
# Pre-phase-3 the exception-only PUT wiped the master.
|
||||
# Pre-phase-4 the master survived but the exception was
|
||||
# invisible in the GET body.
|
||||
# Post-phase-4: both survive AND both are visible.
|
||||
body = _get_master_ical(fresh_calendar, uid)
|
||||
assert "RRULE:FREQ=DAILY;COUNT=10" in body
|
||||
assert "SUMMARY:Daily standup" in body
|
||||
assert "rescheduled" not in body, (
|
||||
"GET on the master URL returned the exception's data — the "
|
||||
"master was clobbered by the exception-only PUT."
|
||||
assert "RRULE:FREQ=DAILY;COUNT=10" in body, (
|
||||
"Master row lost its RRULE — data-loss regression from #528.\n"
|
||||
"Bundle body: " + body
|
||||
)
|
||||
assert "SUMMARY:Daily standup" in body, (
|
||||
"Master's original SUMMARY missing from bundle body — the "
|
||||
"master row was clobbered by the exception-only PUT.\n"
|
||||
"Bundle body: " + body
|
||||
)
|
||||
assert "SUMMARY:Daily standup — rescheduled AGAIN" in body, (
|
||||
"Updated exception SUMMARY missing — the second exception-only "
|
||||
"PUT either failed to update or the emitter dropped the exception "
|
||||
"row from the bundle.\nBundle body: " + body
|
||||
)
|
||||
|
||||
# NOTE: exception-row survival is not asserted client-side
|
||||
# today — the emitter only surfaces the master. Phase 4
|
||||
# (feat/caldav-read-side) will fold master + exceptions into a
|
||||
# single VCALENDAR body; once landed, add an assertion that the
|
||||
# updated exception's SUMMARY ("rescheduled AGAIN") is present
|
||||
# in the same GET body as the master's RRULE.
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@@ -322,7 +333,16 @@ def test_all_day_recurring_master_plus_exception(
|
||||
f"Master body: {data}"
|
||||
)
|
||||
assert "SUMMARY:Weekly review" in data
|
||||
|
||||
# NOTE: exception row is stored server-side but not yet visible
|
||||
# in the GET body. Phase 4 will fold it in — assertion to add
|
||||
# once that lands: assert "RECURRENCE-ID;VALUE=DATE:20260112" in data.
|
||||
# Phase-4 bundle: exception row visible in the GET body.
|
||||
# DATE-form RECURRENCE-ID (with the `;VALUE=DATE` parameter)
|
||||
# survives verbatim because we serve stored ical_data
|
||||
# instead of regenerating.
|
||||
assert "SUMMARY:Weekly review — moved" in data, (
|
||||
"All-day exception SUMMARY missing from bundled GET body:\n"
|
||||
+ data
|
||||
)
|
||||
assert "RECURRENCE-ID;VALUE=DATE:20260112" in data, (
|
||||
"DATE-form RECURRENCE-ID lost — either the exception row "
|
||||
"isn't in the bundle or the emitter mangled the property "
|
||||
"parameter.\nBundle body: " + data
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user